В этой главе мы приступаем, наконец, к самому существенному из того, ради чего писалась эта книга и ради чего, будем надеяться, вы ее читаете. Прежде чем заняться конкретным изучением классов C++, нам хотелось бы немного поговорить об объектно-ориентированном программировании вообще. Конечно, для читателей, которые уже знакомы с его концепциями, следующий раздел будет неинтересен, за что мы заранее извиняемся.
Класс — это множество объектов, имеющих одинаковую структуру. Класс в программировании является аналогом понятия, или категории. В то время как объект представляет собой конкретную сущность, класс является абстракцией сущности объекта. Конкретный объект является представителем, или (не совсем грамотно) экземпляром класса.
Другими словами, структура характеристик объекта и все потенциальные отношения между объектами заложены в классе. Однако классы могут находиться еще и в специфических отношениях между собой.
Наследование
Если существенными видами отношений между объектами являются связь и агрегация, то фундаментальное отношение между классами — это наследование. Один класс может наследовать другому. В C++ в таком случае говорят, что один класс является базовым, а другой (который наследует первому) — производным. Еще их называют соответственно классом-предком и классом-потомком. Наследование может быть прямым, когда один класс является непосредственным предком (потомком) другого, или косвенным, когда имеют место промежуточные наследования.
Производный класс наследует всю структуру характеристик и поведение базового, однако может дополнять или модифицировать их. Если класс В является производным по отношению к А, то с логической точки зрения “В есть А”. Например, понятие, или класс, “автомобиль” (В) является производным по отношению к понятию “средство передвижения” (А). По Л. Н. Толстому, В есть А.
Как и в логике, здесь существует взаимосвязь между “содержанием” и “объемом” понятия. Производный класс имеет большее содержание, но меньший объем, чем базовый.
Наследование может быть простым, когда производный класс имеет всего одного непосредственного предка, и сложным, если в наследовании участвуют несколько базовых классов.
Полиморфизм
Полиморфизм, наряду с наследованием, является фундаментальной концепцией объектной модели программирования. Без него объектно-ориентированное программирование потеряло бы значительную долю своего смысла. Суть полиморфизма в том, что с объектами различных классов, имеющих один и тот же базовый класс, можно при определенных условиях обращаться, как с объектами базового класса; однако объект, являющийся, по видимости, объектом базового класса, будет вести себя по-разному в зависимости от того, что он такое на самом деле, т. е. представитель какого из производных классов.
Существует разные подходы к программированию. Любому из них присущ свой собственный способ абстрагирования сущностей, с которыми он работает. Так, процедурно-ориентированный подход оперирует абстракцией алгоритма. С, кстати, — типичный процедурный язык, хотя на нем возможно писать программы, напоминающие по стилю объектно-ориентированные. Логико-ориентированный имеет в виду цели, выражаемые на языке логических предикатов (таков язык Prolog). Наконец, объектно-ориентированное программирование абстрагирует классы и объекты. В чем же состоит его суть?
Известный теоретик Грейди Буч так определяет этот подход:
“Объектно-ориентированное программирование — это методология программирования, основанная на организации программы, в виде совокупности объектов, каждый из которых является представителем определенного класса, а классы образуют иерархию наследования.”

В “строго” объектно-ориентированных языках объектами (или их составными частями) являются решительно все элементы программы, в том числе она сама. В языке Java, например. Язык C++ таковым, кстати, не является хотя бы потому, что он сохраняет все возможности процедурного языка С. В C++ можно создать, например, совершенно отдельно стоящую глобальную переменную, да и сама функция main)) — совершенно “внеклассовая”. Такая универсальность C++ может быть как преимуществом, так и недостатком, если ею злоупотреблять. У программиста, впервые применяющего объектный подход, всегда имеется тенденция мыслить старыми, процедурными категориями.
Итак, центральным элементом абстракции объектно-ориентированной методологии является, очевидно, объект.
Объекты нашего программного мира моделируют объекты реального или воображаемого мира (например, мира некоторой игры, хотя это мир тоже мыслимый, а стало быть, реальный в смысле логики). Как модель, программный объект представляет собой некоторую абстракцию объекта реального, что предполагает выделение существенных свойств последнего и игнорирование тех, что безразличны с насущной, “сиюминутной”, точки зрения.
Как нечто, как единичность, объект (реальный или программный) отличен от всего остального. Он обладает индивидуальностью, или самотождественностью, если применить опять же термин логики. Понятие индивидуальности программного объекта определить сложно, пожалуй, даже сложнее, чем для реальных объектов, и о нем редко вообще задумываются. Однако интуитивно ясно, что объект, как бы его ни переименовывать и как бы ни менялось его состояние, остается той же самой единичной сущностью с момента создания и до своего уничтожения.
Наконец, заметим, что о программном объекте можно говорить точно так же, как о реальном. Мы можем не только говорить о нем, но и описывать на специальном формальном языке — языке программирования.
В общем, как видно, объект в программе мало чем отличается от предмета реального мира. Он является моделью последнего. Поэтому, в известном смысле, пока не важно, о каких объектах пойдет речь — реальных или программных.
Состояние объекта
Объект, прежде всего, характеризуется своим состоянием. Возьмем, к примеру, базу данных. Она — объект, поскольку есть нечто цельное. К числу моментов, определяющих ее состояние, относится текущее число записей. Каждая запись тоже, очевидно, является объектом. Отдельные поля существующей записи могут модифицироваться, и их совокупность определяет текущее состояние записи, а вместе с ней и состояние всей базы данных.
По-видимому, состояние программного объекта полностью определяется некотором набором (структурой) характеристик и их текущими значениями. Эти характеристики называют полями или (в C++) элементами данных объекта. Но не все моменты состояния объекта должны быть непосредственно видимы извне. Классический пример, который неизменно приводят американские авторы, — автомобиль. С точки зрения водителя у него есть приборный щиток, отражающий скорость, обороты, температуру двигателя и т. д., и органы управления. Но в автомобиле масса частей, спрятанных под капотом, состояние которых не исчерпывается показаниями приборов.
Приведенный ниже код определяет два класса, которые могли бы применяться в графической программе. Это классы точек и линий.
////////////////////////////////////////////////////////////
// Classesl.h: Пример двух геометрических классов. //
const int MaxX = 200; // Максимальные значения координат.
const int MaxY = 200;
//
struct,Point { // Класс точек.
int GetX(void) ;
int GetY(void) ;
void SetPoint(int, int);
private:
int x;
int y;
};
class Line
{
// Класс линий.
Point p0;
Point p1;
public:
Line(int x0, int y0, int xl, int yl);
// Конструктор.
~Line(void); // Деструктор.
void Show(void);
};
Ну вот, такие вот классы. Теперь разберем различные моменты этих определений.

Иногда может потребоваться предварительное объявление класса, если нужно, например, объявить указатель на объект класса прежде, чем будет определен сам класс. Предварительное объявление в этом смысле подобно прототипу функции и выглядит так:
class SomeClass;
Заголовок определения
Определение класса начинается с ключевых слов class, struct или union. Правда, union применяется крайне редко. И структуры, и классы, и объединения относятся к “классовым” типам C++. Разницу между этими типами мы рассмотрим чуть позже.
Спецификации доступа
Ключевые слова private и public называются спецификаторами доступа. Спецификатор private означает, что элементы данных и элементы-функции, размещенные под ним, доступны только функциям-элементам данного класса. Это так называемый закрытый доступ.
Спецификатор public означает, что размещенные под ним элементы доступны как данному классу, так и функциям других классов и вообще любым функциям программы через представитель класса.
Есть еще спецификатор защищенного доступа protected, означающий, что элементы в помеченном им разделе доступны не только в данном классе, но и для функций-элементов классов, производных от него.
Структуры, классы и объединения
Типы, определяемые с ключевыми словами struct, class и union, являются классами. Отличия их сводятся к следующему:
Сначала мы приведем простейший пример определений классов, которые можно было бы разместить в заголовочном файле. Так обычно и делается, если классы должны быть доступны нескольким модулям программы. Следует напомнить, что класс — это расширение понятия типа данных, а точнее, понятия структуры. В C++ принято говорить просто о типах; представитель класса уже нельзя считать просто данными, поскольку ему присуще некоторое поведение.
После этого мы, опираясь на показанный пример, объясним элементарные моменты строения класса в C++.
Виртуальная функция-элемент некоторого класса может быть объявлена чистой. Это выглядит так:
virtual тип имя функции(список параметров} = 0;
Другими словами, тело функции объявляется как ='0 (т. н. чистый спецификатор). Действительная реализация ее не нужна (хотя и возможна). Предполагается, что чисто виртуальная функция будет переопределяться в классах, производных от него. Класс, объявляющий одну или несколько чисто виртуальных функций, является абстрактным базовым классом. Нельзя создать представитель такого класса. Он может служить только в качестве базового для порождения классов, которые полностью реализуют его чисто виртуальные функции.

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

Предположим опять-таки, что вы хотите работать с геометрическими фигурами — линиями, кругами и т. д. Конечно, имеет смысл определить общий базовый класс “фигура”. Тогда можно будет представлять наборы разнородных фигур в виде единообразных списков или массивов указателей. Но нет смысла создавать представителей собственно класса “фигура”. Не бывает в мире фигур просто. Фигура — это абстракция.
С другой стороны, каждый конкретный класс фигур должен иметь метод “нарисовать”. Чтобы этот метод был виртуальным, его должен объявлять базовый класс (как виртуальный, естественно). Но что может делать такой метод, если неизвестно, что рисовать? Конечно, можно написать функцию, которая ничего не делает или возвращает состояние ошибки. Но гораздо лучше объявить такую функцию-элемент базового класса как чистую. Тем самым будет образована виртуальная база для переопределения в производных классах, а также запрещено явное или неявное создание представителей базового класса.
Сказанное иллюстрирует следующий набросок иерархии классов:
class Shape { // Абстрактный базовый класс.
// . . .
public:
virtual -Shape () {} //На всякий случай...
virtual void Draw() =-0; //Чисто виртуальная функция. // . . . };
//
// Производные классы:
//
class Line: public Shape
{ // . . . public:
void Draw() {
// Определение тела
class Rectangle: public Shape
{
// . . .
public:
void Draw()
{
// . . .
}
//...
// И т.д.
Деструктор является противоположностью конструктора. Он вызывается при уничтожении объектов и должен производить необходимую очистку объекта перед освобождением занимаемой им памяти
Именем деструктора должно быть имя класса, которому предшествует тильда (~). Свойства деструкторов таковы:
Деструктор не имеет параметров.
Он не может возвращать значений.
Деструктор не наследуется.
Деструктор не может объявляться как static, const или volatile.
Деструктор может быть объявлен виртуальным.
Ключ доступа определяет “внешний” доступ к элементам базового класса через объекты производного. Что касается доступа самого производного класса к элементам базового класса, то ключ доступа на него не влияет. Для производного класса доступны разделы protected и public базового класса; раздел private строго недоступен вне области действия базового класса.
Для доступа к элементам базового класса через производный можно сформулировать такое правило: права доступа, определяемые для них базовым классом, остаются неизменными, если они такие же или строже, чем специфицировано ключом доступа. В противном случае права доступа определяются ключом в определении производного класса.
Например, при наследовании с ключом public права доступа к элементам базового класса остаются неизменными; при закрытом наследовании (ключ private) все элементы базового класса будут недоступны за пределами производного класса.

При закрытом наследовании можно сделать некоторые открытые функции базового класса открытыми в производном, если переобъявить их имена в производном классе:
class First { public:
void FFunc(void) ;
//... }
class Second: private First { public:
First::FFunc; // First::FFunc() открыта в классе Second.
//.. .
}
Нужно сказать, что в прикладном программировании применяется почти исключительно открытое наследование.
Поскольку функции-элементы класса находятся в его области действия, они могут обращаться к элементам данных непосредственно по имени, как можно видеть в последнем примере. Обычные функции или функции-элементы других классов могут обращаться к элементам существующего представителя класса с помощью операций “.” или “->”:
class Time { public:
int hour;
int min;
} ;
int main()
{
Time start; // Локальный объект Time.
Time *pTime = Sstart; //Указатель на локальный объект.
start.hour = 17; // Операция доступа к элементу.
pTime->min = 30; // Косвенный доступ к элементу.
return 0;
}
Спецификаторы доступа позволяют указать, к каким элементам класса могут обращаться функции, в него не входящие. Однако могут быть случаи, когда целесообразно разрешить некоторому классу или функции обращаться к закрытым или защищенным элементам данного класса. Это можно сделать с помощью ключевого слова friend.
“Друзьями” класса могут быть объявлены другие классы или отдельные функции, как обычные, так и являющиеся элементами некоторых классов. Друзья могут объявляться в любом из разделов определения класса (закрытом, защищенном или открытом), — в каком именно, не имеет значения. В любом случае дружественный класс или функция будет иметь полный доступ к элементам класса.
Вот пример объявления Друзей класса:
class SomeClass (
friend class AnotherClass;
friend void regularFunc (int);
friend void OtherClass::MemFunc(double);
//...
};
Следует иметь в виду такие правила:
Дружественность не обратима: если А объявляет В другом, это не значит, что А будет другом В. Дружба “даруется”, но не “присваивается”.
Дружественность не наследуется: если А объявляет В другом, то классы, производные от В, не будут автоматически друзьями А.
Дружественность не транзитивна: если А объявляет В другом, то классы, производные от А, не будут автоматически признавать В своим другом.
Объекты класса могут быть преобразованы в другие типы (или созданы из других типов) с помощью операций приведения типа или конструкторов преобразования.
Конструкторы преобразования
Если конструктор класса А имеет единственный параметр типа В, то объект В может быть неявно преобразован в класс А с помощью такого конструктора. Другими словами, компилятор может сам вызывать такой конструктор, чтобы “из В сделать А”. Возьмите пример из предыдущего раздела. Локальный объект можно было бы инициализировать по-другому:
class Hold {
char *str;
public:
Hold(const char*);
//...
};
main () {
Hold mainObj = "This is a local object in main.";
//. . .
return 0;
Таким образом, в этом примере объявленный в классе конструктор Hold(const char*) является по сути конструктором преобразования.
Ключевое слово explicit
Можно запретить вызовы конструктора с одним параметром в качестве конструктора преобразования, объявив его с ключевым словом explicit. Тем самым будут запрещены неявные вызовы конструктора, подобные показанному в предыдущем параграфе:
class Hold {
char *str;
public:
explicit Hold(const char*);
//. . .
};
main () {
//
// Неявное преобразование недопустимо:
//
// Hold mainObj = "This is a local object in main.";
//...
return 0;
}
Такой конструктор можно вызывать только явным образом, т. е.
Hold mainObj("This is a local object in main.");
или
Hold mainObj = Hold("This is a local object in main.");

Последняя форма предполагает вызов конструктора копии, который должен обязательно определяться, если класс содержит указатели на динамические объекты подобно классу из листинга 8.1. Там этого не сделано.
Операции приведения
В классе можно определять элементы-функции, которые будут обеспечивать явное преобразование типа данного класса в другие типы. Эти функции называют операциями приведения или процедурами преобразования. Синтаксис их следующий:
operator имя_нового_типа();
Процедуры преобразования характеризуются следующими правилами:
Суммируем и дополним то, что говорилось об элементах данных в предыдущей главе. Элементы данных аналогичны элементам структур языка С. Стоит добавить следующее:
Они не могут быть объявлены как auto, extern или register.
Они могут быть перечислениями, битовыми полями, а также объектами ранее объявленных классов.
Элемент данных класса не может быть представителем самого класса.
Элемент данных класса может быть указателем или ссылкой на сам класс.
Функция-элемент класса объявляется внутри определения класса. Там же может быть расположено и оределение тела функции. В этом случае функцию-элемент называют встроенной и компилятор будет генерировать ее встроенное расширение на месте вызова. Если определение функции располагается вне тела класса, то к ее имени добавляется префикс, состоящий из имени класса и операции разрешения области действия. В этом случае функцию-элемент также можно определить как встроенную с помощью ключевого слова inline. Вот несколько модифицированный класс Point из предыдущей главы вместе с его реализацией:
#include <assert.h>
const int MaxX = 200; // Максимальные значения координат.
const int MaxY = 200;
//
struct Point { // Класс.точек.
private:
int fx;
int fy;
public:
int GetX(void) ( return fx; }
int GetY(void) { return fy; }
void SetPoint(int, int);
};
void Point::SetPoint(int x, int y)
{
assert(x >=0 && x < MaxX);
assert(y >= 0 && у < MaxY);
fx = x;
fy = y;
}
Здесь обе функции Get () определены как встроенные, а функция SetPoint () определяется вне тела класса и не является встроенной.
Как уже говорилось раньше, элементы класса распадаются на две категории. Это данные, инкапсулирующие состояние объектов, и код, отвечающий за их поведение и реализуемый в форме функций-элементов класса.
В языке С выделялось несколько различных типов области действия: глобальная, файл, функция, блок. В C++'вводится новый вид области действия — класс. Имена элементов класса расположены в области действия класса, и к ним можно обращаться из функций-элементов данного класса. Кроме того, получить доступ к элементам класса можно в следующих случаях:
Для существующего представителя класса с помощью операции доступа к элементу (точки).
Через указатель на существующий представитель класса с помощью операции косвенного доступа к элементу (стрелки).
С помощью префикса, состоящего из имени файла и операции разрешения области действия (::).
В прошлой главе мы показали, как определяются простейшие классы C++. То, что содержится в приведенном коде — это интерфейс класса. В самом определении класса объявляются обычно лишь прототипы функций-элементов. Чтобы класс стал работоспособным, необходима еще его реализация. Реализация класса, располагаемая часто в отдельном файле, содержит код его функций-элементов, а также некоторые элементы данных, называемые статическими.
Мы переходим теперь к подробному изучению классов, включая, естественно, и аспекты их реализации.
Константная функция-элемент “обещает” не изменять значений элементов данных класса, что позволяет применять ее на константных объектах. Тем не менее, в некоторых ситуациях имеет смысл разрешить некоторым элементам меняться даже у константных объектов. Например, некоторый существенный набор данных изменять ни в коем случае нельзя, в то время как отдельный элемент, скажем, некоторое сообщение, может и должно меняться. В этом случае можно объявить элемент данных с ключевым словом mutable:
class AnyClass {
int value;
mutable const char *msg;
public.:
AnyClass (): value (0), msg(NULL) {}
int GetValueO const;
// ... };
j nt AnvClass::Get Value() const
{
msg - "New message!";
// Допускается, поскольку msg - mutable.
//
// value изменять нельзя:
//
// value = 111;
//
return value;
}
Модификатор mutable не может применяться совместно с const или static (в приведенном примере все верно, поскольку const относится не к msg, а к содержимому строки, на которую он ссылается).
Можно создать представитель класса с модификатором const. Тем самым гарантируется, что после инициализации содержимое объекта нельзя будет изменить. Компилятор C++Builder выдает предупреждение в случае, если для объекта вызывается функция-элемент, не объявленная как const. Другие компиляторы могут выдать сообщение об ошибке и отказаться транслировать подобный код.
Со своей стороны, константная функция-элемент
объявляется с ключевым словом const после списка параметров;
не может изменять содержимое элементов данных класса;
не может вызывать не-константные функции-элементы. Вот пример константной функции и константного объекта:
class Time {
int hr, min;
public:
Time(int h, int m): hr(h), min(m) {}
void SetTime(int h, int m) { hr = h; min = m;
}
void GetTime(int&, int&) const;
}
void Time::GetTime(int &h, int &m) const {
h = hr;
m = min;
//
// Следующий оператор здесь был бы некорректен:
//
// min = 0;
int main ()
{
Time t(17, 45); // Обычный объект.
const Time ct(18, 0); // Константный объект.
int h, m;
ct.GetTime(h, m); // Вызов const-функции для const-объекта. t.SetTime(h, m) ;
//
// Следующий вызов некорректен:
// // ct.SetTime(0, 0) ;
return 0;
}
Конструктор имеет то же имя, что и класс. Он вызывается компилятором всегда, когда создается новый представитель класса. Если в классе не определен никакой конструктор, компилятор генерирует конструктор по умолчанию (не имеющий параметров).,Относительно конструкторов имеют место такие правила:
Для него не объявляется тип возвращаемого значения.
Он не может возвращать значений оператором return.
Конструктор не наследуется.
Конструктор не может быть объявлен как const, volatile, virtual или static.
Поскольку конструктор не возвращает значений, то для сигнализации об ошибке при инициализации объекта, если требуется, нужно применять механизм управления исключениями, о котором мы будем говорить в 10-й главе.

Можно вызвать конструктор для инициализации уже существующего объекта, если перегрузить глобальную операцию new таким образом, чтобы она принимала дополнительный аргумент — указатель типа void*. Это называют размещающей формой операции; мы о ней уже упоминали в прошлой главе. Такая методика иногда применяется для глобальных представителей класса, если их нужно инициализировать после выполнения каких-то предварительных действий. Вот пример:
#include <new.h>
// Операция new, допускающая форму размещения:
inline void *operator new(size_t, void* p)
{
return p;
}
class Dummy
{
public:
Dummy() // Конструктор.
};
Dummy gblDummy;// Глобальный объект.
int main ()
{
InitSystem(); // Какие-то проверки
// и инициализации.
new(&gblDummy) Dummy; // Вызывает конструктор
// для gblDummy.
return 0;
}
Элементы данных класса часто инициализируют в теле конструктора, присваивая им соответствующие значения. Однако существует альтернативный механизм инициализации. Он использует список инициализации элементов.
Список инициализации следует за заголовком (сигнатурой) определения конструктора после двоеточия и состоит из инициализаторов элементов данных и базовых классов, разделенных запятыми. Каждому элементу списка передается один или несколько параметров, требуемых для инициализации.
Вот простейший пример класса с двумя перегруженными конструкторами, в одном из которых применяется обычный способ инициализации в теле функции, а во втором — список инициализации элементов:
class Time { int hr, min;
public:
Time(int h)
{
hr = h; min = 0;
}
Time(int h, int m): hr(h), min(m)
{
}
};
Тело второго конструктора, как видите, пусто.

Список инициализации является единственным средством присвоения значений элементам данных класса, объявленным как const или как ссылки (а также закрытым элементам базовых классов).
Конструктор копии является конструктором специального вида, который принимает в качестве параметра ссылку или константную ссылку на объект данного класса. Он автоматически вызывается компилятором, когда вновь создаваемый объект инициализируется значениями существующего объекта:
class Time {
int hr, min;
public:
Time(int h, int m): hr(h), min(m) {}
Time(const Time &src) // Конструктор копии.
{ hr = src.hr; min = src.min; } //
};
int main()
(
Time start (17,45); // Вызывается первый конструктор.
Time current = start; // Вызывается конструктор копии.
return 0;
}
Если вы не предусмотрели в классе конструктор копии, компилятор генерирует конструктор копии по умолчанию, который производит простое копирование данных объекта в новый представитель класса. Если класс содержит какие-то указатели или ссылки, то такое копирование скорее всего будет бессмысленным или опасным.

Иногда, когда копирование объектов класса в принципе не может привести ни к чему хорошему, имеет смысл объявить конструктор копии (это может быть просто “пустышка”) в закрытом разделе определения класса. Тогда пользователь класса не сможет создавать копии существующих объектов.
Конструкторы не наследуются. Это утверждение требует некоторых пояснений. Оно означает, что если в базовом классе имеются конструктор с некоторыми параметрами, он не будет вызываться автоматически, если вы . попробуете создать объект производного класса с такими параметрами. Для этого нужно написать конструктор производного класса, в котором конструктор базового класса будет вызываться через посредство списка инициализации. О нем мы уже говорили выше в связи с инициализацией элементов данных класса. Базовые классы в смысле инициализации ничем от них не отличаются.
Если в списке инициализации конструктора отсутствует вызов какого бы то ни было конструктора базового класса, компилятор все равно вызовет для последнего конструктор по умолчанию, т. е. конструктор без параметров. В примере предыдущего параграфа объявлен конструктор базового класса, который может вызываться без параметров, поскольку для него определены аргументы по умолчанию. Если вы не поленитесь пройти по этому примеру в отладчике, то увидите последовательность вызовов при создании объекта производного класса в функции main () .
Однако базовый конструктор можно вызвать явно через список инициализации. Класс из предыдущего параграфа нужно модифицировать примерно так:
class Alarm: public Time { // Класс сообщений таймера.
char *msg;
public:
Alarm(char*);
Alarmfchar*, int, int); // Новый конструктор.
~Alarm() { delete[] msg; }
void SetMsg(char*) ;
void Show(); // Переопределяет Time:: Show ().
//. . .
Alarm::Alarm(char *str, int h, int m): Time(h, m) {
msg = new char[strlen(str) + 1];
strcpy(msg, str);
}
С другой стороны, деструкторы базовых классов никогда явно не вызываются. Деструкторы, можно сказать, даже не имеют имен. Компилятор автоматически генерирует вызовы всех необходимых деструкторов.
Класс в C++ может наследовать элементы-данные и элементы-функции от одного или нескольких базовых классов. Сам класс называется в этом случае производным по отношению к базовым классам или классом-потомком. В свою очередь, производный класс может являться базовым по отношению к другим классам.
Принцип наследования, или порождения новых классов, позволяет абстрагировать (инкапсулировать) некоторые общие свойства и поведение в одном базовом классе, которые будут наследоваться всеми его потомками.
Наследование позволяет также модифицировать поведение базового класса. Производный класс может переопределять некоторые функции-элементы базового класса, оставляя основные свойства класса в неприкосновенности .
Синтаксис производного класса следующий:
class имя класса: ключ доступа имя_базового класса [, ...] {
тело_объявления_класса } ;
Ключ_доступа — это одно из ключевых слов private, protected или public.
При перегрузке операций полезно помнить следующее:
C++ не умеет образовывать из простых операций более сложные. Например, в классе со сложением строк мы определили присваивание и сложение; но это не значит, что тем самым будет автоматически определено присвоение суммы (+=). Такую операцию нужно реализовывать отдельно.
Невозможно изменить синтаксис перегруженных операций. Одноместные операции должны быть одноместными, а двухместные — двухместными.
Нельзя изобретать новые обозначения операций. Возможные операции ограничиваются тем списком, что приведен в начале этого раздела.
Желательно сохранять смысл перегружаемой операции. Например, конкатенация — естественная семантика сложения для строк.
Класс может определять свои собственные операции new и delete (new[] и delete [] для массива объектов):
Функция имя_класса: : operator new () вызывается при создании динамических объектов.
Функция имя_класса: : operator new [ ] () вызывается при создании динамических массивов объектов.
Функция имя_класса:: operator delete() вызывается при удалении динамических объектов.
Функция имя_класса:: operator delete [] () вызывается при удалении динамических массивов.
Ниже приведен довольно длинный пример, демонстрирующий определение операций класса new и delete, а также глобальных new [ ] и delete []. Вывод программы позволяет проследить порядок вызовов конструкторов/деструкторов и операций new/delete при создании автоматических и динамических объектов.
Листинг 8.1. Определение операций класса new и delete
//////////////////////////////////////////////
// NewDel.cpp: Операции класса new и delete.
//
#pragma hdrstop
#include <condefs.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define trace(msg)
printf(#msg "\n")
#define MaxStr 32
void *operator new[](size_t size)
// Замена глобальной
// new[].
{
trace(Global new [ ] .);
return malloc(size);
}
void operator delete[](void *p)
// Замена глобальной
// delete[].
{
trace(Global delete [].);
free(p) ;
}
class Hold {
char *str;
// Закрытый указатель на строку.
public:
Hold(const char*) ;
~Hold() ;
void *operator new(size t);
// Операция new класса.
void operator delete(void*);
// Операция delete класса.
void Show(void)
{ printf("%s\n", str);
}
};
Hold::Hold(const char *s)
// Конструктор.
{
trace (Constructor.) ;
str = new char[MaxStr];
// Вызов глобальной new[].
strncpy(str, s, MaxStr);
// Копирование строки в объект.
str[MaxStr-1] = 0;
// На всякий случай...
}
Hold::~Hold ()
// Деструктор.
{
trace(Destructor.);
delete[] str;
// Очистка объекта.
}
void *Hold::operator new(size_t size)
{
trace(Class new.);


Операция присваивания — это функция-элемент класса с именем operator=, которая принимает в качестве своего единственного параметра ссылку или константную ссылку на объект данного класса. Она вызывается компилятором, когда существующему объекту присваивается другой объект. Если операция присваивания не предусмотрена, компилятор генерирует ее по умолчанию. В этом случае при присваивании будет выполняться поэлементное (как говорят, поразрядное) копирование данных объекта.
Как конструктор копии, так и операция присваивания выполняют, по видимости, одинаковые действия. Однако конструктор копии вызывается при инициализации вновь создаваемого объекта, в то время как операция присваивания служит для изменения содержимого существующих объектов.
Вот пример класса с операцией присваивания:
class Time { int hr, min;
public:
Time(int h, int m): hr(h), min (m) {}
Time &operator=(const Times); // Операция присваивания.
};
Time &Time::operator=(const Time &src)
{
if(&src == this) // Проверка на самоприсваивание.
error("Self assignment!");
hr = src.hr;
min = src.min;
}
return *this; // Возвращает ссылку на свой объект.
int main() {
Time start (17,45);
Time current (18, 0);
start = current; // Вызывает operator=.
return 0;
}
Здесь, кстати, показан прием проверки на самоприсваивание, позволяющей предотвратить присваивание объекта самому себе.

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

Если класс содержит указатели или ссылки, может быть целесообразным, как и в случае конструктора копии, запретить присваивание объектов, объявив операцию присваивания в закрытом разделе класса.
Перегрузка операции вызова operator () () позволяет “вызывать” объект класса, как функцию. Возвращаемое значение будет чем-то вроде значения объекта по умолчанию. Но вообще эта операция может производить любые действия над объектом. Вот пример операции вызова:
class AClass {
int x;
public:
AClass(int n) { x = n; }
int operator ()(void) { return x; }
//. . .
};
int main() {
AClass object = 100;
//...
int у = objectO; // Объект вызывается, как функция.
return 0;
}
Функции-элементы класса могут быть перегружены подобно обычным функциям; несколько функций-элементов могут иметь одно и то же имя, если их можно однозначно идентифицировать по списку аргументов. Вы уже встречались в этой главе с перегруженным конструктором. Это весьма распространенная ситуация. Вот еще подобный пример:
class Time {
long sec; public:
Time(): sec(O) {}
Time(long s): sec(s) {}
Time(int h, int m) {
sec = 3600*h + 60*m;
}
//... };
int main ()
{
Time tl; // Вызывает Time::Time().
Time t2(86399); // Вызывает Time::Time(long).
Time t3(ll, 33); // Вызывает Time::Time(int, int).
//. . .
return 0;
}
| + | — | * | / | % | /\ | & | | | ~ | ! | = | |||||||||||
| < | > | += | -= | *= | /= | %= | ^= | &= | |= | << | |||||||||||
| >> | >>= | <<= | = = | ! = | <= | >= | && | || | ++ | — | |||||||||||
| ' | ->* | -> | () | [] | new | delete | new[] | delete | [ ] |
Язык C++ позволяет переопределять для классов существующие обозначения операций. Это называется перегрузкой операций. Благодаря ей класс можно сделать таким, что он будет вести себя подобно встроенному типу. В классе можно перегрузить любые из следующих операций:
Нельзя перегружать операции:
| . | .* | :: | ?: |
Функции-операции, реализующие перегрузку операций, имеют вид
operator операция ([операнды]) ;

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



При простом, наследовании производный класс порождается всего одним базовым классом. Вот пример:
#include <stdio.h>
#include <string.h>
class Time { // Базовый класс - время.
int hr, min;
public:
Time(int h=12, int m=0): hr(h), min(m) {}
void SetTime(int h, int m) ( hr = h; min = m; }
void Show() ;
};
void Time::Show() {
printf("%02d:%02d", hr, min);
}
class Alarm: public Time { // Класс сообщений таймера.
char *msg;
public:
Alarm(char*);
~Alarm() { delete [] msg; }
void SetMsg(char*);
void Show(); // Переопределяет Time::Show (). };
Alarm::Alarm(char *str) ;
{
msg = new char[strlen (str) + 1];
strcpy(msg, str);
}
void Alarm::SetMsg(char *str) {
delete [] msg;
msg = new char[strlen (str) + 1];
strcpyfmsg, str);
}
void Alarm::Show()
{
Time::Show(); // Вызов базовой Show(). printf(": %s\n", msg);
int main () {
Alarm a = "Test Alarm!!!"; // Время по умолчанию 12:00.
а.Show ();
a.SetTime(7, 40); // Функция базового класса. а.Show () ;
а.SetMsg("It's time!); // Функция производного класса. a.Show();
return 0;
}
Для реализации виртуальных свойств классов нужно обеспечить выбор нужной функции на этапе выполнения программы. Это называют поздним связыванием (объекта с его методами). Компилятор не может заранее разрешить обращение к виртуальной функции-элементу объекта, если последний представлен указателем или ссылкой. На этапе компиляции действительный тип объекта неизвестен. Поэтому компилятор делает примерно следующее:
Для каждого класса, содержащего виртуальные функции, формируется таблица адресов этих функций. Ее называют таблицей виртуальных методов.
Для каждой отдельной иерархии классов адрес конкретной виртуальной функции находится па том же самом месте в таблице виртуальных методов каждого из классов, т. е. имеет одно и то же смещение от начала таблицы.
Каждый представитель класса с виртуальными функциями содержит скрытый указатель на его таблицу виртуальных методов. Компилятор автоматически вставляет в начало конструктора класса код, обеспечивающий инициализацию этого указателя адресом виртуальной таблицы класса.
При вызове виртуальной функции компилятор генерирует код, который прежде всего извлекает из объекта его указатель на виртуальную таблицу. После этого код обращается к пункту таблицы для данной функции, получая, таким образом, нужный адрес. Затем уже несложно генерировать вызов.

Как видите, виртуальный механизм, как и все хорошее в этом мире, связан с некоторыми издержками, как в плане занимаемой памяти (виртуальная таблица), так и в плане времени выполнения (дополнительные косвенные ссылки при вызове). Однако эти издержки, как это ни удивительно, очень малы.
Заключение
Содержание данной главы практически исчерпывает объектно-ориентированные аспекты C++. Однако в языке еще немало различных возможностей, среди которых можно назвать управление исключительными ситуациями и шаблоны. Одним из таких аспектов C++ являются классы потоков ввода-вывода, изучением которых мы займемся в следующей главе. Реализация ввода-вывода может служить примером использования ряда объектно-ориентированных концепций, описанных выше.
Язык C++ допускает не только простое, но и сложное наследование, т. е. наследование от двух и более непосредственных базовых классов. Это позволяет создавать классы, комбинирующие в себе свойства нескольких независимых классов-предков.
Это чаще всего имеет смысл, когда у вас есть некоторый набор понятий, которые могут более или менее независимо комбинироваться с различными элементами другого набора понятий. Простейший пример. Имеется понятие “растение”. Бывают “культурные растения”, и бывают “дикорастущие растения”. С другой стороны, растение может иметь или не иметь “товарной ценности”, т. е. быть полезным или бесполезным с коммерческой точки зрения. Если говорить о товарной ценности, то тут у растений бывают “цветы” и “плоды” и т. д. Все это образует довольно развернутую структуру, которая может порождать понятия вроде “дикое растение, цветы которого можно продавать на рынке”. (Возможно, кстати, и такое: “дикое растение, цветы которого имеют товарную ценность, но которые нельзя продавать на рынке”!) А можно, с некоторыми модификациями, говорить то же самое не о растениях, а о животных или веществах, минералах. И есть не только “товарные” сущности, но и сорняки, вредители. И так далее.
Очевидно, здесь существует ряд довольно независимых категорий — “растение”, “товар”, “культурность” и прочее. Подобная структура — отличный кандидат на реализацию в виде иерархии классов со сложным наследованием.

Кстати, в языке Object Pascal, реализованном в сходном продукте Борланда — Delphi, — нет сложного наследования, что в ряде случаев может значительно ограничить его полезность в сравнении с C++.
Для иллюстрации сложного наследования возьмем последний пример с “сообщениями таймера”. Понятия времени и понятие сообщения — независимые, и, возможно, в программе будут другие классы, родственные “времени” и “сообщению”. Поэтому вполне разумным будет определить для них отдельные классы и породить от них третий класс, применив методику сложного наследования:
#include <stdio.h>

Специальными функциями-элементами называют функции, которые могут вызываться компилятором неявно. Это может происходить при создании и уничтожении представителей класса, при их копировании и преобразовании в другие типы. К таким функциям относятся:
Конструктор. Инициализирует представители класса.
Конструктор копии. Инициализирует новый представитель, используя значения существующего объекта.
Операция присваивания. Присваивает содержимое одного представителя класса другому.
Деструктор. Производит очистку уничтожаемого объекта.
Операция new. Выделяет память для динамически создаваемого объекта.
Операция delete. Освобождает память, выделенную под динамический объект.
Функции преобразования. Преобразуют представитель класса в другой тип (и наоборот).
Статический элемент данных является по существу глобальной переменной с областью действия в классе и разделяется всеми представителями класса. Он только один, вне зависимости от того, сколько представителей имеет класс. На самом деле статический элемент данных существует даже в том случае, когда никаких представителей класса не создано.
Помимо объявления в определении класса, статический элемент данных должен еще и определяться:
class SomeClass
{
static int iCount;
// Объявление статического
// элемента.
//.. .
};
int SomeClass::iCount = 0;
// Определение статического
// элемента.

Обращаться к открытым статическим элементам класса можно либо через любой его представитель операциями “.” и “->”, либо с помощью операции разрешения области действия (SomeClass : : iCount). Последний способ предпочтительнее, так как ясно показывает, что элемент не связан с конкретным объектом.
Функция класса, объявленная с модификатором static, не связывается ни с какими его конкретными представителями. Другими словами, ей не передается указатель this в качестве скрытого параметра. Это означает, что:
Статическая функция-элемент может вызываться, даже если никаких
Статическая функция-элемент может обращаться только к статическим элементам данных класса и вызывать только другие статические функции-элементы класса.
Такая функция не может быть объявлена виртуальной.

Статические функции-элементы класса могут передаваться процедурам API Windows в качестве возвратно-вызываемых, поскольку не предполагают наличия на стеке параметра this. Обычные функции-элементы для этого не годятся.
Можно объявить элемент класса (данные или функцию) как статический.
Любая функция-элемент класса, не являющаяся статической (что это такое, выяснится позднее) имеет доступ к объекту, для которого она вызвана, через посредство ключевого слова this. Типом this является имя_класса*.
class Dummy {
void SomeFunc(void) {...};
public:
Dummy();
};
Dummy::Dummy)
{
SomeFunc();
this->SomeFunc();
(*this).SomeFunc();
}
В этом примере каждый оператор конструктора вызывает одну и ту же функцию SomeFunc (). Поскольку функции-элементы могут обращаться к элементам класса просто по имени, подобное использование указателя this довольно бессмысленно. Это ключевое слово чаще всего применяется для возврата из функции-элемента указателя или ссылки на текущий объект (вы увидите это позже, когда будут рассматриваться функции-операции).

На самом деле this является скрытым параметром, передаваемым функции-элементу класса при вызове. Именно этим функция-элемент (не статическая) отличается от обычной функции. При вызове функции-элемента компилятор генерирует код, который после всех указанных в вызове параметров помещает на стек указатель на объект, для которого функция вызвана. Поэтому, например, нельзя вызвать функцию-элемент через обычный указатель на функцию. Указатель на функцию-элемент объявить можно, но в нем явно должен быть специфицирован класс, например:
long (Dummy::*fPtr)();
fPtr = &Dummy::SomeFunc;
Привести такой указатель к обычному типу указателя на функцию невозможно. При его разыменовании всегда нужно указывать конкретный объект, и для этого в C++ предусмотрены две новых операции “. *” и “->*”. Первая из них применяется с непосредственным объектом, вторая — с указателем на объект. Вызов фун.кции-элемента через указатель выглядит так:
1 = (dummyObj.*fptr)();
1 = (dummyPtr->*fptr)();
Все сказанное приложимо и к элементам данных. На них тоже можно ссылаться с помощью указателей, которые нельзя привести к обычному типу указателя. На самом деле они содержат не адрес некоторой ячейки памяти, а смещение элемента данных относительно начала объекта. Для разыменования указателей на элемент данных используются те же операции:
double Dummy::*dptr;
dptr = &Dummy::someData;
d = dumrnyObj . *dptr;
d = duinmyPtr->*dptr;
Приоритет и правило ассоциации у этих специфических операций те же, что и у обычного разыменования (*) и других одноместных операций (14, справа налево).
Функции-элементы класса могут объявляться в C++ как виртуальные. Ключевое слово virtual заставляет компилятор генерировать для класса некоторую дополнительную информацию 6 функции. Происходит следующее: если виртуальная функция переопределяется в производном классе, и если имеется указатель или ссылка на базовый класс (которые могут с тем же успехом ссылаться на производный класс, поскольку производный объект есть в то же время и объект базового класса), то при обращении к функции через указатель (ссылку) будет вызвана правильная функция-элемент (т. е. соответствующая типу действительного объекта) — базового или одного из производных классов, в зависимости от типа конкретного объекта.

Не хочу показаться занудой, но, как мне кажется, стоит повторить, чем косвенное обращение к объекту (указатель или ссылка) в данном отношении отличается от прямого. Можно недвусмысленно объявить объект базового и объект производного классов. Потом можно присвоить объект производного класса переменной базового типа. Не требуется даже никаких приведений, потому что, как я уже говорил, производный объект является объектом базового класса. “Автомобиль” есть “средство передвижения”. Однако при этом будет потеряна всякая специфика “автомобиля”, отличающая его от всех других средств передвижения, наземных, водных или воздушных. Но применение указателей или ссылок в объектно-ориентированных языках типа C++ приводит к тому, что объект сам может помнить, к какому типу он относится, и указатель на базовый тип может быть в данном случае снова приведен
Следующий пример покажет вам разницу между виртуальным и не виртуальным переопределением функции.
//////////////////////////////////////////////////////////
// Virtual.cpp: Демонстрация виртуальной функции.
//
#pragma hdrstop
#include <condefs.h>
#include <stdio.h>
class One
{
// Базовый класс. public:
virtual void ShowVirtO
// Виртуальная функция.
{
printf("It's One::ShowVirt()!\n");
}
void ShowNonVirt() // Не-виртуальная функция.



Совершенно аналогично тому, что имеет место в случае элементов-данных, функции-элементы класса могут вызываться функциями-элементами того же класса просто по имени. Обычные функции и элементы других классов могут вызывать функции-элементы данного класса для существующих его представителей с помощью операций “ . ” или “->” (через указатель). Приведенный ниже пример это иллюстрирует.
#include <stdio.h>
class Time ( int hour;
int min;
public:
void SetTime(int h, int m)
{
hour = h; min = m; } void ShowTime(void)
{
printf("Time: %02d:%02d.\n", hour, min);
}
};
int main()
{
Time start;
Time *pStart = &start;
int hr, min;
start.SetTime(17, 15); // Вызов элемента для объекта
// start.
pStart~>ShowTime(); // вызов элемента через указатель
//на объект.
return 0;
}
До сих пор речь у нас шла почти исключительно о вводе-выводе с использованием операций извлечения/передачи данных. Эти операции перегружены для всех встроенных типов и выполняют соответствующие преобразования из внутреннего представления данных в текстовое и из текстового во внутреннее (машинное).
Однако в библиотеке C++ имеется немало функций бесформатного ввода-вывода, которые часто применяют для чтения и записи двоичных (не-текстовых) файлов.
Чтение сырых данных производится функцией read () класса istream:
istream &read(char *buf, long len);
Здесь buf — адрес буфера, в который будут читаться данные, а len — число символов, которые нужно прочитать.
Запись сырых данных производится функцией write () класса ostream. Она выглядит точно так же, как функция read () :
ostream &write(char *buf, long len);
Здесь buf — адрес буфера, в котором содержатся данные, а len — число символов, которые должны быть записаны в поток.
Обе функции возвращают ссылку на свой объект-поток. Это означает, что возможны их цепные вызовы, т. е. выражения вроде
ostream os (...);
os.write(...).write (...).write(...) ;
Вот небольшой пример записи и чтения сырых данных:
#include <iostream.h>
#include <fstream.h>
int main(void) {
char name[] = "testfile.dat";
int i = 1234567;
double d = 2.718281828;
//
// Открытие выходного потока в двоичном режиме
//и запись тестовых данных.
//
ofstream ofs(name, ios::out | ios::binary);
if (ofs) {
ofs.write((char*)&i, sizeof(i)); // Целое.
ofs.write((char*)&d, sizeof(d)); // Вещественное.
ofs.write(name, sizeof(name)); // Строка. ofs.close ();
}
//
// Открытие входного потока в двоичном режиме.
// if stream ifs(name, ios::in | ios::binary) ;
i = 0; //
d = 0; // Уничтожить данные.
name[0] = '\0'; //
//
// Прочитать данные.
//
if (ifs) {
ifs.read((char*)&i, sizeof (i));
ifs.read((char*)&d, sizeof(d));
ifs.read(name, sizeof(name));
ofs.close () ;
} //
// Проверка - напечатать прочитанные данные. //
cout “ "Data read from file: i = " << i<< ", d = " << d
<< ", name = " << name << endl;
return 0;
}
Для чтения одиночных символов, а также строк применяется функция get класса istream. Эта функция перегружена следующим образом:
int get () ;
istream &get(char &c) ;
istream &get(char *buf, long len, char t = '\n');
Две первые формы функции предназначены для извлечения из потока одиночного символа. Функция int get() возвращает символ в качестве своего значения. Функция get (char &c) передает символ в параметре и возвращает ссылку на свой поток.
Вот, например, как можно было бы выполнить посимвольное копирование файлов:
ifstream ifs("infile.dat");
ofstream ofs("outfile.dat");
while (ifs & ofs)
ofs.put(ifs.get());
// put (char) передает в поток
// одиночный символ.
Последняя форма функции get () извлекает из потока последовательность символов. Символы читаются в буфер buf, пока не произойдет одно из следующих событий:
будет встречен ограничивающий символ t (по умолчанию ' \n ');
будет встречен конец файла;
в буфер будет записано len символов, включая завершающий строку 0. Имеется еще функция getline(), во всем аналогичная этой форме
get ():
istream Sgetline(char *buf, long len, char t = '\n');
Разница между этими двумя функциями состоит в том, что get line () извлекает из потока ограничивающий символ, в то время как get () этого не делает. Ни та, ни другая функция не записывает ограничивающий символ в буфер.
Пример использования getline () вы уже видели в листинге 9.4. Вот небольшой пример чтения строк с помощью get ():
#inciude <iostream.h>
int main(void) {
char name[9], ext[4];
cout << "Enter a filename with extension: ";
cin.get(name, 9, '.');
cin.ignore (80, '.'); // Удалить все оставшиеся
// до точки символы. cin.get(ext, 4) ;
cin.ignore(80, '\n'); // Удалить все, что осталось
// в строке.
cout<< "Name: "<< name << "; extension: " << ext << endl;
return 0;
}
Эта программа, как нетрудно догадаться, усекает произвольное имя файла до формата 8.3.
Двоичный режим открытия файла (с установленным битом binary) означает, что никакой трансляции данных при передаче из файла в поток и обратно производиться не будет. Речь здесь идет не о форматных преобразованиях представления данных. При текстовом режиме (он принимается по умолчанию) при передаче данных между файлом и потоком производится замена пар символов CR/LF на единственный символ LF (' \n ') и наоборот. Это происходит до преобразований представления, которые выполняются операциями извлечения/передачи. Двоичный ввод-вывод означает всего-навсего, что такой замены происходить не будет; тем не менее двоичный режим необходим при работе с сырыми данными, т. е. данными в машинной форме без преобразования их в текстовый формат.
Чтобы открыть файл в двоичном режиме, нужно, как уже упоминалось, установить в параметре mode конструктора потока или функции open() бит ios::binary.
Файловые потоки библиотеки ввода-вывода реализуют объектно-ориентированную методику работы с дисковыми файлами. Имеется три класса таких потоков:
ifstream специализирован для ввода из дисковых файлов.
of stream специализирован для записи дисковых файлов.
fstream управляет как вводом, так и записью на диск.
Эти классы выводятся соответственно из istream, ostream и iostream. Таким образом, они наследуют все их функциональные возможности (перегруженные операции << и>>” для встроенных типов, флаги форматирования и состояния, манипуляторы и т. д.).
Чтобы работать с файловым потоком, нужен, во-первых, объект потока, а во-вторых, открытый файл, связанный с этим объектом.
Библиотека ввода-вывода предусматривает три способа форматирования: посредством вызова форматирующих функций-элементов, с помощью манипуляторов или путем установки или сброса флагов потока.
Флаги управления форматированием являются битовыми полями, хранящимися в переменной типа fmtflags (псевдоним int). Для их чтения и/или модификации могут применяться следующие функции-элементы класса ics:
int flags (), int flags (int). Без параметра возвращает текущее состояние флагов. При указанном параметре устанавливает новые значения флагов и возвращает их прежнее состояние.
int setf(int), long setf(int, int). Первая форма устанавливает флаги, биты которых установлены в параметре. Вторая форма модифицирует флаги, биты которых установлены во втором параметре. Значения этих флагов задаются первым параметром. Возвращает прежнее состояние всех флагов.
void unsetf(int). Сбрасывает флаги, биты которых установлены в параметре.
Помимо функций, для управления флагами можно пользоваться манипуляторами setiosflags (аналог setf() с одним параметром) и reset-iosflags (аналог unsetf ()).
В таблице 9.3 описаны форматирующие флаги потоков.
Таблица 9.3. Форматирующие флаги класса ios
| Флаг | Описание | ||
| internal | Если установлен, при выводе чисел знак выводится на левом краю поля вывода, а само число выравнивается по правому краю поля. Промежуток заполняется текущим символом заполнения. | ||
| dec | Устанавливает десятичное представление чисел. Принимается по умолчанию. | ||
| oct | Устанавливает восьмеричное представление чисел. | ||
| hex | Устанавливает шестнадцатеричное представление чисел. | ||
| showbase | Если установлен, то при восьмеричном и шестнадцатеричном представлении чисел выводит индикатор основания (0 для восьмеричных и Ох для шестнадцатеричных чисел). | ||
| showpoint | Если установлен, для вещественных чисел всегда выводится десятичная точка. | ||
| uppercase | Если установлен, шестнадцатеричные цифры от А до F, а также символ экспоненты Е выводятся в верхнем регистре. | ||
| boolalpfa | Если установлен, булевы значения выводятся как слова “true/false”. В противном случае они представляются соответственно единицей и нулем. | ||
| showpos | Выводит + для положительных чисел. | ||
| scientific | Если установлен, вещественные числа выводятся в научной (экспоненциальной) нотации. | ||
| fixed | Если установлен, вещественные числа выводятся в десятичном формате (с фиксированной точкой). | ||
| unitbuf | Если установлен, поток сбрасывается после каждой операции передачи. |


Эти функции являются элементами класса ios и перегружены таким образом, чтобы можно было либо читать, либо устанавливать значение соответствующего атрибута потока. Если аргумент в вызове отсутствует, функция возвращает текущее значение атрибута. Если аргумент указан, функция устанавливает новое и возвращает предыдущее значение атрибута.
long width(long)
Эта функция предназначена для чтения или установки атрибута ширины поля.
Применяемая ко входному потоку, функция позволяет задать максимальное число вводимых символов.
На выходном потоке функция задает минимальную ширину поля вывода.
Если действительное поле вывода меньше установленной ширины, выводятся дополнительные заполняющие символы. Символ заполнения определяется специальным атрибутом потока.
Если действительное поле вывода больше установленной ширины, ее значение игнорируется.
Значением ширины по умолчанию является 0 (ширина поля определяется выводимыми данными).
Ширина поля сбрасывается в 0 после каждой передачи в поток.
char fill(char)
Функция позволяет прочитать или установить текущий символ заполнения.
По умолчанию символ заполнения — пробел.
long precision(long)
Эта функция позволяет прочитать или установить значение атрибута точности, определяющего либо общее число выводимых цифр, либо число цифр дробной части.
Точность по умолчанию равна шести цифрам.
Если установлен флаг scientific или fixed, точность задает число цифр после десятичной точки.
Если ни один из этих флагов не установлен, точность задает общее число значащих цифр.
Пример
Ниже приводится программа, демонстрирующая форматирование потока с помощью функций-элементов класса ios.
Листинг 9.1. Демонстрация форматирующих функций потока
///////////////////////////////////////////////
// Format.срр: Форматирующие функции-элементы ios.
//
#include <iostream.h>
#pragma hdrstop
#include <condefs.h>
#pragma argsused
int main(int argc, char* argv[])
{
//
// Ширина поля при вводе и выводе.
//
cnar sir [16];
До сих пор в программных примерах мы пользовались только функциями стандартной библиотеки С. Однако в C++ имеются собственные средства, основанные на принципах классовой модели. Другими словами, в исполнительной библиотеке C++ имеется набор классов для управления вводом-выводом.
В отличие от функций буферизованного ввода-вывода С (таких, как print f и scanf, не выполняющих никаких проверок на соответствие аргументов форматной строке) классы потоков C++ безопасны в отношении типа. Ввод-вывод использует механизм перегрузки операций, гарантирующий вызов нужной функции-операции для указанного типа данных. Это главное преимущество потоков языка C++.
К классам потоков относятся следующие:
Класс streambuf управляет буфером потока, обеспечивая базовые операции заполнения, опорожнения, сброса и прочих манипуляций с буфером.
Класс ios является базовым классом потоков ввода-вывода.
Классы istream и ostream — производные от ios и обеспечивают работу потоков соответственно ввода и вывода.
Класс iоstream является производным от двух предыдущих и предусматривает функции как для ввода, так и для вывода.
Классы ifstream, of stream и f stream предназначены для управления файловым вводом-выводом.
Классы istrstream и ostrstream управляют резидентными потоками (форматированием строк в памяти). Это устаревшая методика, оставшаяся в C++Builder в качестве пережитка.
Для работы с потоками вам потребуется включить в программу заголовочный файл iostream.h. Кроме того, может потребоваться подключить файлы fstream.h (файловый ввод-вывод), iomanip.h (параметризованные манипуляторы) и strstream.h (форматирование ь памяти).
Каждый из трех классов файловых потоков имеет четыре конструктора.
Конструктор, создающий объект без открытия файла:
ifstream () ;
of stream();
fstream () ;
Конструктор, создающий объект, открывающий указанный файл и закрепляющий этот файл за потоком. Аргументами являются имя файла, режим открытия и режим защиты (в Windows не используется):
if stream(const char *name,
int mode = ios::in, long prot = 0666);
ofstream(const char *name,
int mode = ios::out, long prot = 0666);
fstream (const char *name, int mode, long prot = 0666);
Конструктор, создающий объект и связывающий с ним уже открытый файл. В качестве аргумента передается дескриптор файла:
ifstreamfint file);
ofstream(int file);
fstream (int file) ;
Конструктор, создающий объект и связывающий с ним уже открытый файл; объект ассоциируется указанным буфером:
ifstream(int file, char *buf, int len)
of stream(int file, char *buf, int len)
fstream (int file, char *buf, int len)
Манипуляторы потоков являются по существу функциями, которые можно вызывать непосредственно в цепочке операций извлечения или передачи в поток. Различают простые и параметризованные манипуляторы. У простых манипуляторов аргументы отсутствуют. Параметризованные манипуляторы имеют аргумент.
Ниже приводится сводка имеющихся манипуляторов, как простых, так и параметризованных. Они Перечислены в алфавитном порядке.
Таблица 9.2. Простые и параметризованные манипуляторы
| Манипулятор | Описание | ||
| dec | Задает десятичную базу преобразования. | ||
| end1 | Передает в поток символ новой строки и сбрасывает поток. | ||
| ends | Передает в поток символ завершающего строку нуля. | ||
| flush | Сбрасывает выходной поток. | ||
| hex | Задает шестнадцатеричную базу преобразования. | ||
| lock(ios Sir) | Блокирует дескриптор файла потока ir. | ||
| oct | Задает восьмеричную базу преобразования. | ||
| resetiosflags(int f) | Сбрасывает флаги, биты которых установлены в f. | ||
| setbase(int b) | Устанавливает базу преобразования (0, 8, 10 или 16). | ||
| setiosflags(int f) | Устанавливает флаги, биты которых установлены в f. | ||
| setfill(int c) | Задает символ заполнения (аналогичен функции
fiilO). | ||
| setprecision(long p) | Задает точность (аналогичен функции precision ()). | ||
| setw(iong w) | Задает ширину поля (аналогичен функции width ()). | ||
| lunlock(ios &ir) | Разблокирует дескриптор файла для потока ir. | ||
| ws | Исключает начальные пробельные символы. |
Вот пример использования некоторых манипуляторов (мы создали один свой собственный):
Листинг 9.2. Форматирование с помощью манипуляторов
/////////////////////////////////////////////////
// Manip.cpp: Демонстрация некоторых манипуляторов.
//
#include <iomanip.h>
#pragma hdrstop
#include <condefs.h>
//////////////////////////////////////////////////
// Манипулятор, определенный пользователем - звонок.
//
ostream shell(ostream &os)
{
return os<< '\a';
#pragma argsused
int main(int argc, char* argv[])
{
cout “ bell; // Тестирование манипулятора bell.


В классах istream и ostream есть ряд функций, которые позволяют выполнять над потоками разные полезные операции (в основном при бесформатном вводе-выводе). Здесь мы опишем наиболее часто употребляемые из них.
Класс istream
Следующие функции являются элементами класса istream:
istream &ignore(long n = 1, int t = EOF) ; Эта функция позволяет пропустить при вводе ряд символов. Она извлекает из потока максимум n символов, пока среди них не встретится ограничитель t. Ограничитель также извлекается из потока.
long gcount() ; Функция возвращает число символов, извлеченных из потока последней функцией неформатированного ввода. (Если после нее выполнялись какие-либо форматные процедуры, это число может быть модифицировано.)
int peek () ; Функция возвращает следующий символ потока, не извлекая его. Возвращает EOF, если установлен какой-либо из флагов состояния потока.
istream Sputback (char с) ; Возвращает во входной поток последний извлеченный символ.
istream &seekg(long pos) , istream &seekg(long ofs, int dir) ;
Устанавливает положение указателя потока. Для первой формы указывается абсолютная, для второй — относительная позиция указателя. Параметр dir может принимать следующие значения:
ios : :beg смещение (ofs) от начала файла; ios : : cur смещение от текущей позиции указателя;
ios : : end смещение от конца файла.
long tellg() ; Возвращает текущее положение указателя входного потока.
Класс ostream
Последним двум функциям из istream соответствуют аналогичные функции класса ostream:
ostream &seekp(long pos), ostream &seekp(long ofs, int dir) ; Аналогична see kg () . Выполняет абсолютное или относительное позиционирование выходного потока.
long tellp() ; Аналогична tellg() . Возвращает текущую позицию выходного потока.
Основными классами ввода-вывода C++ являются istream и ostream. Первый из них перегружает операцию правого сдвига (>>), которая служит в нем для ввода данных и называется операцией извлечения из потока. Класс ostream перегружает соответственно операцию левого сдвига (<<); она применяется для вывода и называется операцией передачи в поток.

Нужно сказать, что стандартной русской терминологии как таковой в C++ не существует. Каждый изобретает свою собственную; иногда удачно, иногда — нет.
Вот простейшие операторы ввода и. вывода на стандартных потоках:
#include <iostream.h>
int main()
{
char name [ 8.0] ;
cout<< "Enter your name: ";
cin>> name;
cout <<"Hello " << name << "!";
return 0;
}
Как видите, при действиях над потоками возможно последовательное сцепление операций, подобно последовательному присваиванию. Как вы уже знаете, такая форма записи обеспечивается благодаря тому, что функции-операции извлечения и передачи возвращают ссылку на свой объект.
Классы istream и ostream перегружают операции соответственно извлечения и передачи в поток для всех встроенных типов. Это позволяет единообразно применять эти операции для чтения и вывода символов, целых, вещественных чисел (т. е. с плавающей точкой) и строк. Вот небольшая иллюстрация, где попутно показан еще простейший прием проверки на ошибку при вводе:
#include <iostream.h>
void check(void) {
if (!cin.good())
{
// Либо просто if (!cin) {
cout << "Error detected!";
exit (1);
}
int main(void)
{
double d;
long 1;
cout << "Enter a floating point value: ";
cin >> d;
check () ;
cout << "You entered: " << d << '\n';
cout << "Enter an integer value: ";
cin >> 1;
check () ;
cout << "You entered: " << 1 << '\n';
return 0;
}

Операции извлечения и передачи в поток (соответственно для классов istream и ostream) можно перегрузить таким образом, чтобы можно было применять их для ввода или вывода объектов класса, определенного пользователем. Приведенный ниже пример демонстрирует эту методику. Вообще-то в подобных случаях совершенно необходимо предусмотреть детектирование и обработку ошибок ввода, но здесь мы этого не сделали.
#include <iostream.h>
class Point { int x, у;
public:
Point(int xx = 0, int yy = 0) {
x = xx; у = yy;
}
friend istream &operator>>(istream&, Points);
friend ostream &operator“(ostream&, Points);
};
istream &operator”(istream &is, Point &p)
//
// При вводе точка представляется просто парой чисел,
// разделенных пробелом.
// is >> р.х > р.у;
return is;
}
ostream &operator<<(ostream &os.Point &p) {
//
// Вывод в виде (х, у).
//
os<< ' ( '<< р. х<< ", "<< р. у<<') ' ;
return os;
}
int main() {
Point р;
cout<< "Enter point coordinates: ";
cin>> р;
cout<< "The point values are " << р;
return 0;
}
Библиотека ввода-вывода C++ предусматривает четыре предопределенных объекта-потока, связанных со стандартными входным и выходным устройствами. Ниже дана сводка этих объектов.
Таблица 9.1. Предопределенные объекты-потоки C++
| Имя | Класс | Описание | |||
| cin | istream | Ассоциируется со стандартным вводом (клавиатурой). | |||
| cout | ostream | Ассоциируется со стандартным выводом (экраном). | |||
| cerr | ostream | Ассоциируется со стандартным устройством ошибок (экраном) без буферизации. | |||
| clog | ostream | Ассоциируется со стандартным устройством ошибок (экраном)с буферизацией. |
Следующий пример (листинг 9.4) демонстрирует различные режимы и способы открытия потока.
Листинг 9.4. Примеры открытия файловых потоков
/////////////////////////////////////////////////////////
// Filemode.срр: Режимы открытия файлов.
//
#include <f stream .,h>
#include <string.h>
#pragma hdrstop
#include <condefs.h>
char *data[] = {"It's the first line of test data.",
"Second ,line.",
"Third line.",
"That's enough!"};
//
// Функция для распечатки содержимого файла. //
int Print(char *fn) {
char buf[80] ;
ifstream ifs(fn) ;
if (!ifs) {
cout <<fn<< " - Error reading file." << endl;
return -1;
} while (ifs) {
ifs.getline(buf, sizeof(buf)) ;
if (ifs)
cout << buf<< end1;
} return 0;
}
#pragma argsused
int main(int argc, char* argv[])
{
char name[]= "Newfile.txt";
fstream fs(name, ios::in);
if (fs) { // Файл уже существует. cout “ name “ " - File already exists." << endl;
} else { // Создать новый файл.
cout<< name<< " - Creating new file."<< endl;
fs.open(name, ios::out);
for (int i=0; i<3; i++) fs << data[i] << endl;
}
fs.close () ;
cout << end1;
//
// Файл либо уже существовал, либо мы его только что
// создали. Распечатаем его.
// Print(name);
cout << endl;
//
// Допишем строку в конец файла.
// fs.open(name, ios::app);
if (rs) {
fs M<< data[3]<< endl;
fs.close ();
} Print(name);
return 0;
}

Рис. 9.3 Результат работы программы Filemode
Для чтения строки из файла мы применили в программе функцию getline () , которая будет подробно описана чуть позже.
Параметр mode, который имеет вторая форма конструктора, задает режим открытия файла. Для значений параметра класс ios определяет символические константы, перечисленные в таблице 9.5.
Таблица 9.5. Константы класса ios для режимов открытия файла
| Константа |
Описание |
||
| арр | Открытие для записи в конец файла. | ||
| ate | При открытии позиционирует указатель на конец файла. | ||
| binary | Файл открывается в двоичном (не текстовом) режиме. | ||
| in | Файл открывается для ввода. | ||
| out | Файл открывается для вывода. | ||
| trunc | Если файл существует, его содержимое теряется. |
Константы можно комбинировать с помощью поразрядного OR. Для конструкторов классов if stream и ofstream параметр mode имеет значения по умолчанию — соответственно ios : : in и ios : : out.
Состояние объекта класса ios (и производных от него) содержится в его закрытом элементе _state в виде набора битов. Следующая таблица перечисляет имеющиеся биты состояния потока.
Таблица 9.4. Биты состояния потока
| Бит |
Описание |
||
| goodbit | С потоком все в порядке (на самом деле это не какой-то бит, а 0 — отсутствие битов ошибки). | ||
| eofbit | Показывает, что достигнут конец файла. | ||
| failbit | Индицирует ошибку формата или преобразования. После очистки данного бита работа с потоком может быть продолжена. | ||
| badbit | Индицирует серьезную ошибку потока, связанную обычно с буферными операциями или аппаратурой. Скорее всего, поток далее использовать невозможно. |
Для опроса или изменения состояния потока в классе ios имеется ряд функций и операций.
int rdstate() ; Возвращает текущее состояние.
bool eof() ; Возвращает true, если установлен eofbit.
bool good () ; Возвращает true, если не установлен ни один из битов ошибки.
bool fail () ; Возвращает true, если установлен failbit или bad-bit.
bool bad() ; Возвращает true, если установлен badbit.
void clear (int =0); Сбрасывает биты ошибки (по умолчанию) или устанавливает состояние потока в соответствии с аргументом.
void setstate(int) ; Устанавливает состояние битов ошибки с соответствии с аргументом.
operator void*() ; Возвращает нулевой указатель, если установлен какой-либо из битов ошибки.
bool operator! () ; Возвращает true, если установлен какой-либо из битов ошибки.

Функция operator void*() неявно вызывается, если поток сравнивается с нулем (как cin в примере из листинга),
Понятие произвольного доступа к файлу подразумевает два, или даже три, различных момента. Во-первых, оно означает, что можно произвольно обращаться к любой записи или любому байту в файле, в противоположность последовательному доступу, когда данные извлекаются или передаются в поток строго по очереди. Во-вторых, предполагается, что на открытом файле можно произвольно чередовать операции чтения и записи. И, наконец, из сказанного вытекает, что ввод-вывод с произвольным доступом является по преимуществу бесформатным.
Приведенная ниже программа открывает (создает новый или переписывает старый) свой файл как двоичный, и, кроме того, сразу для ввода и вывода. Она применяет функции позиционирования потока и функции бесформатного чтения-записи.
Листинг 9.5. Произвольный доступ к файлу
//////////////////////////////////////////////////
// Random.cpp: Демонстрация файла с произвольным доступом.
//
#include <fstream.h>
#include <iomanip.h>
#pragma hdrstop
#include <condefs.h>
const int NP = 10;
const int IS = sizeof(int);
#pragma argsused
int main(int argc, char* argv[])
{
int pt, i;
//
// Открытие файла для чтения/записи.
//
fstream fs("random.pts",
ios::binary | ios::in | ios::out | ios::trunc);
if (ifs) {
cerr << "Failed to open file." << endl;
return (1);
}
//
// Первоначальная запись файла.
//
cout << "Initial data:" << endl;
for (i=0; i<NP; i++){
pt = i;
fs.write((char*)&pt, IS);
cout << setw(4) << pt;
}
cout << endl << endl;
//
// Чтение файла от конца к началу.
//
cout << "Read from the file in reverse order:"<< endl;
for (i=0; i<NP; i++) {
fs.seekg(-(i + 1) * IS, ios::end);
fs.read((char*)&pt, IS);
cout “ setw(4)<< pt; . }
cout<< end1 << end1;
//
// Переписать четные индексы.
//
for (i=l; i<NP/2; i++) {
fs.seekg(2 * i * IS) ;
fs.read((char*)&pt, IS);
pt = -pt;
fs.seekg(fs.tellg () - IS); // Возврат на шаг.



В классах файловых потоков имеется функция close (), которая сбрасывает содержимое потока и закрывает ассоциированный с ним файл.
Кроме того, деструктор потока автоматически закрывает файл при уничтожении объекта потока.
При ошибке закрытия файла устанавливается флаг failbit.