[Оглавление]
[Вниз]

В определенном выше простейшем классе complexl есть общедоступные компонентные данные типа double, поэтому следующее определение указателя pdafc будет вполне правильным:

complexl соmр; соmр. rеа1=16.0; comp. imag=33 .4;
double (complexl: *pdat) = &complexl: comp.imag;
cout << "\ncomp. imag = " << comp. *pdat;
pdat = &complexl::comp. real; // Перестройка указателя
cout << "\ncomp.real = " << comp.*pdat;

Результат выполнения приведенных операторов:

comp. image =33.4
comp.real =16

Указатель на компонент класса можно использовать в качестве фактического параметра при вызове функции.

В приведенных примерах мы использовали операцию '. * ' разыменования указателей на компоненты класса:

имя_объекта. *указатель_на_компонент,_данных
имя_ объекта.*указаталь_ на_ метод (параметры)

Слева от операции '. * ' кроме имени конкретного объекта может помещаться ссылка на объект.

Если определен указатель на объект класса и введены указатели на компоненты того же класса, то доступ к компонентам конкретных объектов можно получить с помощью бинарной операции ' ->* ' доступа к компонентам класса через указатель на объект:

указатель_на_объект__класса ->*указатель_на_компонент_данных
указатель_на_объект_класса ->*указатель_на_метод (параметры)

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

Второй (правый) операнд - указатель на компонент класса. Результат выполнения операции ' ->* ' -это либо компонент данных, либо компонентная функция класса. Если второй операнд - это лево допустимый компонент данных, то и результат применения операции ' ->* ' ( а также операции '. * ') есть l-значение. Например, определим и инициализируем указатель pcml на компонент данных класса complexl:

double (complex1: : *pcm1) = #complex1::real;

Определим и инициализируем указатель pcomplexl на объекты класса complexl:

complex1 CM(10.2,-6.4);
complex1 *рсоmр1ех1 = &СМ;

Теперь, применяя операцию ' ->* ', получим l-значение:

pcomplex1->*pcm1 = 22.2;

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

Если справа от операции ' ->* ' находится инициализированный указатель на компонентную функцию для того объекта, на который "настроен" левый операнд, то выполнится обращение к соответствующему методу:

complex А(22.2,33.3); // Объект класса
complex *рСоmрlех = &А.; // Указатель класса
void (complex::*pdisplay) (); // Указатель на компонентную
// функцию
pdisplay = &complex::display; // "Настройка" указателя
(pComplax->*pdisplay) (); // Вызов компонентной функции
// через указатель на объект
// класса и указатель на
// компонентную функцию

В данном примере на экран выводится сообщение:

real = 22.2, imag = 33.3

9.4. Определение компонентных функций

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

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

При определении классов их компонентные функции также могут быть специфицированы как подставляемые. Кроме явного использования служебного слова inline дм этого используется следующее соглашение. Если определение (не только прототип) принадлежащей классу функции полностью размещено в классе (в теле класса), то эта функция по умолчанию считается подставляемой. Именно таким образом определены компонентные функции классов complexl, goods, complex, stroka, point3, использованных ранее в качестве примеров. Все функции перечисленных классов воспринимаются компьютером как подставляемые, т.е. при каждом вызове этих функций их код "встраивается" непосредственно в точку вызова. Как уже говорилось в связи с обсуждением особенностей подставляемых функций, это не всегда удобно, так как подставляемыми могут быть не всякие функции. (Подставляемая функция не может быть рекурсивной, не может содержать циклы, переключатели и т.д.) Это ограничение весьма существенное, поэтому существует второй способ определения принадлежащих классу функций. Он состоит в том, что внутри тела класса помещается только прототип компонентной функции, а ее определение - вне класса, как определение любой другой функции, входящей в программу. При таком внешнем определении компонентной функции она также может быть снабжена спецификатором inline, но при этом опять возникнут указанные ограничения, накладываемые на подставляемые функции.

При внешнем определении компонентной функции программист "должен сообщить" компилятору к какому именно классу она относится. Для этого используется бинарная форма операции ':: ' (указания области видимости). Формат ее использования в этом случае таков:

имя_класса:: имя_компонентной_функции

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

Итак, при внешнем определении компонентной функции в теле класса помещается ее прототип:

тип имя_ функции(спецификация_ и_инициализация параметров);

Вне тела класса компонентная функция определяется таким образом:

тип имя класса::
имя_функции (спецификация_формальных_параметров)
{тело_принадлежащей_классу_функции}

В качестве примера класса с внешним определением компонентных функций введем класс point, определяющий понятие "точка на экране дисплея". Разместим описание класса в отдельном файле с названием point.h:

// POINT.Н - описание класса с внешними определениями
// методов
#ifndef POINTH
#define POINTH 1
class point { // Точка на экране дисплея
protected: // Защищенные данные класса:
int x, у; // Координаты точки
// Прототипы общедоступных компонентных функций:
public:
point(int xi = О, int yi = 0); // Конструктор
int& givex(void); // Доступ к x
int& givey(void); // Доступ к у
void show(void); // Изобразить точку на экране
// Переместить точку в новое место экрана:
// (хn == 0, yn == 0 - умалчиваемые значения
// параметров)
void move(int хn = 0, int уn = 0);
private: // Собственная функция класса:
void hide (); // Убрать с экрана изображение точки
} ;
#endif

Так как описание класса point в дальнейшем планируется включать в другие классы, то для предотвращения недопустимого дублирования описаний в текст включена условная препроцессорная директива #ifndef POINTH. Препроцессорный идентификатор POINTH определяется тут же с помощью директивы

#define POINTH 1

Тем самым текст описания класса point может появляться в компилируемом файле только однократно, несмотря на возможность неоднократных появлений директив #include "point, h". Обратите внимание на обычную необязательность имен формальных параметров при описании прототипов функций. В прототипе конструктоpa и в прототипе функции move () имена формальных параметров xi, yi, xn, yn можно было бы опустить.

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

Для работы с графической библиотекой компиляторов фирмы Borland в программу должен быть обязательно включен заголовочный файл graphics. h. После этого в тексте программы можно обращаться к библиотечным графическим функциям и использовать заранее подготовленные константы и структуры. Учитывая сказанное, определим компонентные функции класса point следующим образом:

// POINT.СРР - внешнее определение функций класса
#ifndef POINTCPP
#define POINTCPP 1
#include < graphica.h > // Связь с графической библиотекой
#include "point.h" // Описание класса point
point::point(int xi=0, int yi=0)
{ x = xi; у = yi;} // Определение данных объекта
int& point::givex(void) { return x; } // Доступ к х
int& point::givey(void) { return y; } // Доступ к у
// Изобразить точку на экране:
void point::show(void)
{ putpixel (x, у, getcolor ()) ; }
// Убрать с экрана изображение точки:
void point::hide(void)
{ putpixel(x,у,getbkcolor()); }
// Переместить точку в новое место экрана:
void point::move(int xn=0, int yn=0)
{ hide();
x = xn; у = yn;
show();
}
#endif

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

void putpixel(int x, int у, int color)

изображает цветом color точку на экране дисплея с координатами (х. y);

int getbkcolor(void)

возвращает номер цвета фона;

int getcolor(void)

возвращает номер цвета изображения.

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

Для иллюстрации в данном определении класса конструктор point и прототип функции move () снабжены умалчиваемыми значениями параметров. Координаты создаваемой по умолчанию (без указания значений параметров) точки равны нулю. Туда же по умолчанию перемещается точка.

Внешнее определение методов класса в противоположность встроенному позволяет модифицировать принадлежащие классу функции, не изменяя текста описания класса.

Определив класс и его компонентные функции, приведем программу, иллюстрирующую работу с классом point:

//Р9-04.СРР - работа с классом "точка на экране"
#include < graphics.h > // Прототипы графических функций
#include < conio.h > // Прототип функции getch ( )
#include "point.cpp" // Определение класса point
void main()
{ point A(200,50); // Создается невидимая точка А
point В; // Невидимая точка В с нулевыми
// координатами по умолчанию
point D(500,200); // Создается невидимая точка D
// Переменные для инициализации графики:
int dr = DETECT, mod;
initgraph(&dr,&mod,"c:\\borlandc\\bgi ");
A.show (); // Показать на экране точку А
getch (); // Ждать нажатия клавиши
В.show (); // Показать на экране точку В
getch(); D.show(); // Показать на экране точку D
getch (); A.move (); // Переместить точку А
getch (); B.move(50, 60); // Переместить точку В
getch (); closegraph (); // Закрыть графический режим
}

Результаты выполнения программы показаны на рис. 9.1.

Рис. 9.1. Последовательность изображений на экране при выполнении программы Р9-04. CPP

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

Переключение дисплея в графический режим выполняет специальная функция initgraph (), вызов которой осуществляется в основной программе. Ей всегда нужно передать три фактических параметра. Первый из них по соглашениям графической библиотеки проще всего инициализировать стандартным именем DETECT, которое определено в заголовочном файле graphics. h. Второй параметр - это переменная типа int. Третий параметр - символьная строка (указатель на нее), явно определяющая путь к графической библиотеке. В примере библиотека находится на диске с: в каталоге BORLANDC\BGI (подробнее см. [6,31]).

Выполнение программы понятно из комментариев. Как объекты класса point создаются невидимые точки А, B, D, затем они изображаются на экране и перемещаются по нему.

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

В программе используется еще одна библиотечная функция getch(), прототип которой находится в заголовочном файле conio.h. При обращении к этой функции программа "останавливается" и ожидает ввода от клавиатуры любого символа. Тем самым у пользователя появляется возможность проследить смену изображений на дисплее. Функция getch() специфична для MS-DOS.

9.5. Указатель this

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

имя_ класса * const this = адрес_ обрабатываемого_ объекта;

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

struct ss { int si; char sс;
ss (int in, char cn) // Конструктор объектов класса
{ this->si = in; this->sc = cn;}
void print(void) // Функция вывода сведений об объекте
{ cout << "\n si = " << this->si;
cout << "\n sc = " << this->sc;
}
};

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

//Р9-05.СРР - указатель "this"
#include < iostream.h >
class cell // Класс "числовой элемент"
{ int static Amount; // Общее количество элементов
int Number; // Порядковый номер элемента
double Meaning; // Значение элемента
public:
// Конструктор:
cell (double Mеaning =0.0)
{ // Меняем значение статического компонента:
Amount++;
this -> Number = Amount;
// Компонент и одноименный параметр:
this -> Meaning = Meaning;
}
// Вывод сведений об объекте и количестве элементов:
void display(void)
{ cout << "\nNumber = " << this->Number;
// Лишний this:
cout << " Amount = " << this->Amount;
cout << " Meaning = " << this->Meaning;
}
} ;
// Инициализация статического компонента:
int cell::Amount = 0;
void main (void)
{ cell A; // Объект с умалчиваемым значением
A.display ();
cell В(200.0);
cell С(300.0);
В.display ();
С.display ();
}

Результат выполнения программы:

Number = 1     Amount = 1     Meaning = 0
Number = 2     Amount = 3     Meaning = 200
Number = 3     Amount = 3     Meaning = 300

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

имя_класса::имя_компонента

В приведенном примере класса cell конструктор может быть и таким:

cell(double Meaning = 0.0)
{ Amount++;
Number = Amount;
cell::Meaning = Meaning; // Устранение неоднозначности
}

Почти незаменимым и очень удобным указатель this становится в тех случаях, когда в теле принадлежащей классу функции нужно явно задать адрес того объекта, для которого она вызвана. Например, если в классе нужна функция, помещающая адрес выбранного объекта класса в массив или включающая конкретный объект класса в список, то такую функцию сложно написать без применения указателя this. Действительно, при организации связных списков, звеньями которых должны быть объекты класса, необходимо включать в связи звеньев указатель именно на тот объект, который в данный момент обрабатывается. Это включение должна выполнить некоторая функция-компонент класса. Однако конкретное имя включаемого объекта в момент написания этой принадлежащей классу функции недоступно, так как его гораздо позже произвольно выбирает программист, используя класс как тип данных. Можно передавать такой функции ссылку или указатель на нужный объект, но гораздо проще использовать указатель this. Итак, повторим, когда указатель this использован в функции, принадлежащей классу, например, с именем ZOB, то он имеет по умолчанию тип ZOB * const и всегда равен адресу того объекта , для. которого вызвана компонентная функция. Если в программе для некоторого класса х определить объект

X factor(5);

то при вызове конструктора класса х , создающего объект factor, значением указателя this будет &f actor.

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

//MEMBER.H - "элементы двухсвязного списка"
class member
{ // Адрес последнего элемента списка:
static member *last_memb;
member *prev ; // На предыдущий элемент списка
member *next; // На следующий элемент списка
char bulcva; // Содержимое (значение) элемента списка
public:
// Функции для работы со списком:
member (char cc) { bukva = cc; } // Конструктор
void add(void); // Добавление элемента в конец списка
// Вывод на дисплей содержимого списка:
static void reprint(void);
};

Из объектов класса member, как из звеньев, может формироваться двухсвязный список. Схема построения списка показана на рис. 9.2. В классе member имеется статический компонент-указатель last-memb на последний объект, уже включенный в список. Когда список пуст, значение last_memb должно быть равно нулевому указателю NULL. Связь между объектами как звеньями списка организуется с помощью указателей next и prev. Пустой список (last_memb==NULL) представлен на рис. 9.2. Выполняет "подключение" объекта к списку компонентная функция add (). Статическая функция reprint () позволяет "перебрать" звенья списка (объекта класса member) в порядке от конца к началу и вывести символы ("содержания") объектов на экран дисплея.Конструктор инициализирует компонент char bukva каждого создаваемого объекта.

last_memb = NULL                                                                                               Пустой список

Рис. 9.2. Последовательность формирования списка из объектов класса membег(P - previous (предыдущий), К - next (следующий))

Определим компонентные функции класса member:

//MEMBER.СРР - определения функций класса member
#include < iostream.h >
#include < stdio.h > // Для описания нулевого указателя NULL.
// определение класса с прототипами функций:
#include "mеmbеr.h"
// Добавление элемента в конец списка:
void member::add(void)
{ if (last_ mеmb == NULL) this -> prev = NULL;
else { last_memb -> next = this;
this -> prev = last_memb;
}
last mеmb = this; this->next = NULL;
}
// Вывод на дисплей содержимого списка:
void member::reprint(void)
{ member *uk; // Вспомогательный указатель
uk = last_ memb;
if (uk == NULL) { cout << "\nСписок пуст!"; return; }
else cout << " \nСодeржимое списка :\n";
// Цикл печати в обратной порядке значений элементов
// списка:
while (uk != NULL)
{ cout << uk -> bukva << '\t'; uk = uk -> prev;}
}

Вне класса указатель last_memb до включения в список первого элемента инициализируется нулевым значением (NULL). Поэтому первым шагом выполнения функции add будет проверка значения last_memb. Если он равен нулю, то в список включается первый элемент (объект), для которого указатель prev на предшествующий элемент должен быть нулевым. Для подключения объекта к уже существующему списку необходимо указателю next последнего в списке объекта присвоить значение указателя this (адрес добавляемого объекта). В качестве указателя на своего предшественника (prev) подключаемый объект получает значение last_memb. Затем последним становится обрабатываемый (только что подключенный) объект (last_memb - this;) и обнуляется его указатель next на последующий объект в списке.

Компонентная функция reprint () описана в классе как статическая. Это никак не сказывается на ее определении. Первое действие функции - "настройка" вспомогательного указателя их на последний включенный в список объект. Его адрес всегда является значением указателя last_memb. Если список пуст, то на этом выполнение функции завершается. В противном случае в цикле печатаются значения uk->bukva и указатель "перемещается" к предыдущему звену списка.

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

//Р9-06.СРР - статические компоненты, указатель this
#include < iostream.h >
#include "member.cpp" // Определение класса member
// Инициализация статического компонента (указателя):
member *mеmbеr: :last_ mеmb = NULL;
void main ()
{ // Формирование объектов класса mеmbеr:
mеmbеr А('а'); member В('b');
member C('c'); member D('d');
// Вызов статической компонентной функции:
member::reprint ();
// Включение созданных объектов в двусвязный список:
A.add (); B.add (); C.add () ; D.add ();
// Печать в обратном порядке значений элементов списка:
member::reprint();
}

Результат выполнения программы:

Список пуст!
Содержимое списка:
d     с     b     a

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

9.6. Друзья классов

Как уже сказано, механизм управления доступом позволяет выделять общедоступные (public), защищенные (protected) и собственные (private) компоненты классов. Защищенные компоненты доступны внутри класса и в производных классах. Собственные компоненты локализованы в классе и недоступны извне. С помощью общедоступных компонентов реализуется взаимодействие класса с любыми частями программы. Однако имеется еще одна возможность расширить интерфейс класса. Ее обеспечивают дружественные функции. По определению [2], дружественной функцией класса называется функция, которая, не являясь его компонентом, имеет доступ к его защищенным и собственным компонентам. Функция не может стать другом класса "без его согласия". Для получения прав друга функция должна быть описана в теле класса со спецификатором friend. Именно при наличии такого описания класс предоставляет функции права доступа к защищенным и собственным компонентам. Пример класса с дружественной функцией:

//Р9-07.СРР - класс с дружественной функцией
#include < conio.h > // Для консольных функций в текстовом
// режиме
// Класс - "символ в заданной позиции экрана":
class charlocus
{ int х, у; // Координаты знакоместа на экране дисплея
// Значение символа, связанного со знакоместом:
char cc;
// Прототип дружественной функции для замены символа:
friend void friend_put(charlocus *, char);
public:
charlocus(int xi, int yi, char ci) // Конструктор
{ x = xi; у = yi; cc = ci;}
void display (void) // Вывести символ на экран
{gotoxy(x,y); putch(cc); }
};
// Дружественная функция замены символа в конкретном
// объекте:
void friend_put(charlocus *p, char с)
{ р->сс = с; }
void main (void)
{ charlocus D(20,4,'d'); // Создать объект
charlocus S (10,10, 's') ; // Создать объект
clrscr(); // Очистить экран
D. display (); getch (); S. display (); getch ();
friend_put(&D,'*'); D.display (); getch ();
friend_put(&S, '#') ; S.display (); getch ();
}

Программа последовательно выводит на экран d (в позицию 20, 4), s (в позицию 10,10), * (в позицию 20,4), # (в позицию 10,10).

Для работы с экраном в текстовом режиме использованы две функции из библиотеки Turbo С. Их прототипы находятся в заголовочном файле conio.h, где специфицированы так называемые "консольные" функции ввода-вывода. В других компиляторах языка Си++ эти функции могут быть определены иначе.

void gotoxy (int x, int у)

позволяет поместить курсор в позицию экрана с "координатами" х (по горизонтали) и у (по вертикали). В обычном (текстовом) режиме количество строк 25 (у меняется от 0 до 24), количество столбцов 80 (х меняется от 0 до 79). Позиция с координатами (0,0) соответствует левому верхнему углу экрана.

void putch (int s)

выводит на экран в местоположение курсора изображение символа, код которого определяется значением параметра s.

int getch(void)

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

Более подробно с консольными функциями можно познакомиться по документации компилятора либо по имеющимся публикациям.

Функция friend_put () описана в классе charlocus как дружественная и определена как обычная глобальная функция (вне класса, без указания его имени, без операции:: и без спецификатора friend). Как дружественная она получает доступ к собственным данным класса и изменяет значение символа того объекта, адрес которого будет передан ей как значение первого параметра.

Выполнение основной программы очевидно. Создаются два объекта D и S, для которых определяются координаты мест на экране и символы (d, s). Затем общедоступная функция класса charlocus:: display () выводит символы в указанные позиции экрана. Функция friendJput заменяет символы объектов, что демонстрирует повторный вывод на экран.

Отметим особенности дружественных функций. Дружественная функция при вызове не получает указателя this. Объекты классов должны передаваться дружественной функции только явно через аппарат параметров. При вызове дружественной функции нельзя использовать операции выбора:

имя объекта.имя_ функции

и

указатель_на_объект -> имя_функции

Все это связано с тем фактом, что дружественная функция не является компонентом класса. Именно поэтому на дружественную функцию не распространяется и действие спецификаторов доступа (public, protected, private). Место размещения прототипа дружественной функции внутри определения класса безразлично. Права доступа дружественной функции не изменяются и не зависят от спецификаторов доступа. В приведенном примере описание функции friend_put () помещено в разделе, который по умолчанию имеет статус доступа private.

Итак, дружественная функция

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

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

В качестве примера рассмотрим дружественную функцию двух классов "точка на плоскости" и "прямая на плоскости". Класс "точка на плоскости" включает компонентные данные для задания координат (х, у) точки. Компонентными данными класса "прямая на плоскости" будут коэффициенты А, B, C общего уравнения прямой А *х+В*у+С=0. Дружественная функция определяет уклонение заданной точки от заданной прямой. Если (а, b) - координаты конкретной точки, то для прямой, в уравнение которой входят коэффициенты А, B, C, уклонение вычисляется как значение выражения А*а+в*b+с.В следующей программе определены классы с общей дружественной функцией, в основной программе введены объекты этих классов и вычислено уклонение от точки до прямой:

//Р9-08.СРР - классы с общей дружественной функцией
#include < iostream.h >
class line2; // Предварительное описание
// Класс "точка на плоскости":
class point2
{ float х, у; // Координаты точки на плоскости
public:
point2(float xn = 0, float уn = 0) // Конструктор
{ х = хn; у = уn;)
friend float uclon(point2,line2);
};
// Класс "прямая на плоскости":
class line2 { float А, В, С; // Параметры прямой
public:
line2(float a, float b, float с) // Конструктор
{А = а; В = b; С = с;}
friend float uclon(point2,line2);
};
// Внешнее определение дружественной функции
float uclon(point2 р,line2 1)
{ return 1.A * р.х + 1.B * р.у + 1.С;}
void main(void)
{ point2 Р(16.0,12.3); // Определение точки Р
line2 L(10.0,-42.3,24.0); // Определение прямой L
cout << "\n Уклонение точки Р от прямой L:";
cout << uclon(P,L);
}

Результат выполнения программы:

Уклонение точки Р от прямой L: -336.290009

В качестве упражнения можно вместо дружественной функции uclon () определить глобальную функцию с теми же параметрами. При этом в классы point2 и line2 придется ввести дополнительные компонентные функции для доступа к собственным данным.

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

class X2 { friend class X1; ... } ;
class X1 { ... // Определение дружественного класса
void f1(...);
void f2(...);
...
};

В данном примере функции fl и t2 из класса x1 являются друзьями класса х2, хотя они описываются без спецификатора friend.

Все компоненты класса доступны в дружественном классе. Дружественный класс может быть определен позже (ниже), нежели описан как дружественный.

В качестве примера "дружбы" между классами рассмотрим класс pointN - "точка в N-мерном пространстве" и дружественный ему класс vectorN - "радиус-вектор точки" ("вектор с началом в начале координат N-мерного пространства"). Все компоненты точки - ее размерность Npoint и массив координат х[Npoint] - собственные, и доступ к ним в классе vectorN возможен только за счет дружеских отношений. Конструктор класса pointN выделяет память для массива координат и инициализирует этот массив заданным значением параметра double d; Конструктор класса vectorN формирует объект "радиус-вектор" или просто "вектор" по двум объектам класса pointM, проверяя равенство их размерностей. Объекты класса pointN задают начало и конец вектора, который затем приводится к началу координат. Кроме конструктора в классе vectorN введена функция для определения нормы вектора, которая вычисляется как сумма квадратов координат его конца. В основной программе сформирован вектор по двум точкам 2-мерного пространства, затем сделана неправильная попытка создать вектор из двух точек разной размерности. Текст программы:

//Р9-09.СРР - дружественные классы
#include < iostream.h >
#include < stdlib.h > // Для функции exit ()
// Класс "точка в N-мерном пространстве":
class pointN
{ int Npoint; // Размерность пространства
double *x; // Указатель на массив координат точки
// Описание дружественного класса:
friend class vectorN;
public:
pointN (int n, double d = 0.0); // Конструктор "точек"
};
// Определение конструктора:
pointN::pointN(int n, double d)
{ Npoint = n; // Определение размерности
// Выделение памяти для координат:
х = new double [Npoint];
for (int i = 0; i < Npoint; i++)
x[i] = d; // Инициализация массива координат
}
// Класс "радиус-вектор":
class vectorN
{ // Указатель на массив координат конца вектора:
double *xv;
int Nvector; // Размерность пространства
public:
vectorN(pointN,pointN); // Конструктор "векторов"
double norm (); // Норма вектора
} ;
// Определение конструктора:
vectorN : : vectorN(pointN beg,pointN end)
{ if (beg.Npoint != end.Npoint) // Проверка точек
{ cerr << "\n0шибка в размерностях точек !";
exit(1); // Завершение программы
}
Nvector = beg.Npoint; // Размерность вектора
xv = new double [Nvector];
for (int i = 0; i < Nvector; i++)
xv[i] = end.x[i] - beg.x[i]; // Определение координат
}
double vectorN::norm () // Вычисление нормы вектора
{ double dd = 0.0;
for (int i = 0; i < Nvector; i++)
dd += xv[i] * xv[i];
return dd;
}
void main(void)
{ pointN A(2,4.0);
pointN В(2,2.0);
vectorN V(A,B) ;
cout << "\nНорма вектора: " << V. nоrm ();
pointM X(3,2.0);
vectorN Z(A,X);
}

Результат выполнения программы:

Норма вектора: 8
Ошибка в размерностях точек!

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

9.7. Расширение действия (перегрузка) стандартных операций

Одной из привлекательных особенностей языка Си++ является возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально в языке не предполагались. Например, если s1 и S2 - символьные строки, то их конкатенацию (соединение) удобно было бы обозначить как s1 + S2. Однако бинарная операция + в обычном контексте языка Си++ предназначена для арифметических операндов и не предусматривает строковых операндов. Никакой возможности распространить действие стандартной операции + на строки в виде символьных массивов или строковых констант в языке Си++ нет. Однако, если определить s1 и S2 как объекты некоторого класса, например, введенного в п. 9.2 класса stroka, то для них можно ввести операцию +, выполняемую по таким правилам, которые заранее выбрал программист. Для этих целей язык Си++ позволяет распространить действие любой стандартной операции на новые типы данных, вводимые пользователем. Распространить операцию на новые типы данных позволяет механизм перегрузки стандартных операций.

Чтобы появилась возможность использовать стандартную для языка Си++ операцию (например, ' + ' или ' * ') с необычными для нее данными, необходимо специальным образом определить ее новое поведение. Это возможно, если хотя бы один из операндов является объектом некоторого класса, т.е. введенного пользователем типа. В этом случае применяется механизм, во многом схожий с механизмом определения функций. Для распространения действия операции на новые пользовательские типы данных программист определяет специальную функцию, называемую "операция-функция" (operator function). Формат определения операции-функции:

тип_ возвращаемого_значения operator знак_операции
(спецификация_параметров_операции-функции)
{операторы_ тела_ операции-функции}

При необходимости может добавляться и прототип операции-функции с таким форматом:

тип_ возвращаемого_ значения operator знак_ операции
(спецификация_параметров_операции-функции);

И в прототипе, и в заголовке определения операции-функции используется ключевое слово operator, вслед за которым помещен знак операции. Если принять, что конструкция operator знак_операции есть имя некоторой функции, то определение и прототип операции-функции подобны определению и прототипу обычной функции языка Си++. Например, для распространения действия бинарной операции ' * ' на объекты класса т может быть введена функция с заголовком

Т operator *(T x, Ту)

Определенная таким образом операция (в нашем примере операция "звездочка") называется перегруженной (по-английски - overload), а сам механизм - перегрузкой или расширением действия стандартных операций языка Си++.

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

Если для класса т введена операция-функция с приведенным выше заголовком и определены два объекта А, в класса т, то выражение А * в интерпретируется как вызов функции operator *(A,B). В качестве содержательного примера распространим действие операции '+' на объекты класса "символьные строки". Для этого используется определенный выше в п. 9.2 класс stroka, в котором len -длина строки и ch - указатель на символьный массив с текстом строки. В классе stroka два конструктора. Один для создаваемого объекта выделяет память заданных размеров и оформляет ее как пустую строку. Второй формирует объект класса stroka по уже существующей строке, заданной в качестве фактического параметра. Вне класса определим операцию-функцию с заголовком

stroka& operator +(stroka& A, stroka& В),

распространяющую действие операции '+' на объекты класса stroka. Определение операции-функции размещено ниже основной программы, в которой используется выражение с операцией ' + ', примененной к объектам класса stroka. Указанное размещение текста определения операции-функции потребовало применения ее прототипа, который помещен до функции main (). Текст программы:

//Р9-10.СРР - расширение действия (перегрузка) операции '+'
// Определение класса "символьные строки":
#include "stroka.cpp"
// Прототип функции для расширения действия операции '+'
stroka& operator +(stroka& A, stroka& В);
void main (void) { stroka X("Qui");
stroka Y(" Vivra");
stroka Z(" Verra!");
stroka C;
C=X+Y+Z+"- Поживем - увидим!";
С.display ();
}
// Расширение действия операции + на строковые операнды:
stroka& operator +(stroka& a, stroka& b)
{ // Длина строки-результата:
int ii = a.len_str() + b.len_str ();
stroka *рs; // Вспомогательный указатель
// Создаем объект в динамической памяти:
ps = new stroka(ii);
// Копируем строку из 'а':
strcpy(ps->string (),а.string() );
// Присоединяем строку из 'b':
strcat(ps->string (),b.string() );
ps->len_str() = ii; // Записываем значение длины строки
return *рs; // Возвращаем новый объект stroka
}

Результат выполнения программы:

Длина строки: 36
Содержимое строки: Qui Vivra Verra! - Поживем - увидим!

В программе операция-функция, расширяющая действие операции ' + ' на операнды типа stroka&, используется трижды в одном выражении

X+Y+Z+" - Поживем - увидим! "

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

operator знак_ операции(фактические параметры);

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

с = operator + (X, Y);
С = operator + (C,Z);
С = operator + (С," - Поживем - увидим!");

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

Т operator &(T х)

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

Проиллюстрируем особенности расширения действия бинарной операции с помощью компонентной функции. Введем операцию '+' для точек на экране дисплея, определяемых классом point (см. выше). Для краткости упростим определение класса point, оставив только самые необходимые компоненты, но дополнительно введем операцию-функцию, расширяющую действие бинарной операции ' + ':

//Р9-11.СРР - компонентная функция для перегрузки
// операции '+'
#include < graphics.h > // Графическая библиотека
class point1 // Точка на экране
{ // Защищенные компоненты (данные) класса:
protected:
int х, у; // Координаты точки
// Общедоступные принадлежащие классу функции:
public:
point1(int xi = 0, int yi = 0) // Конструктор
{ х = xi; у = yi;);
// Изобразить точку на экране:
void show(void) ( putpixel(x,y,getcolor() }; } ;
// Прототип компонентной операции-функции:
pointi operator +(pointl& p);
};
// Внешнее определение компонентной операции-функции:
point1 poinfcl::operator +(point1 &p)
{ pointi d;
d.x = this->x + p.x;
d.y = this->y + p.y;
return d;
}
#include < conio.h > // Для функции getch()
void main()
{ // Переменные для инициализации графики:
int dr = DETECT, mod;
point1 A(200,50); // Создаются еще невидимые точки
// Точка создается с умалчиваемыми координатами:
pointi В;
point! D(50,120); // Инициализация графической системы:
initgraph(&dr,&mod,"c:\\borlandc\\bgi");
A.show(); // Показать на экране точку А
getch(); // Ждать нажатия клавиши
В.show(); getch();
D.show(); getch();
В = А + D; // Неявное обращение к операции-функции
B.show (); getch();
В = A.operator + (В); // Явный вызов операции-функции
В.show (); getch();
closegraph(); // Закрыть графический режим
}

Рис. 9.3. Последовательность изображений на экране при выполнении программы Р9-11. СРР

В результате выполнения программы на экран дисплея (рис. 9.3) выводятся последовательно точки: А(200, 50); в(0, 0); D(50, 120); в(250, 70), в(450,220).

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

point1 *ptr = &А; // Указатель "настроен" на
// объект А класса
point1 В = ptr->operator + (D); // Операция '+' выполняется, как
// А + D

В предыдущей программе операция-функция для перегрузки операции ' + ' явно вызвана для объектов А и В.

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

//Р9-12.СРР - операция-функция как дружественная функция
#include < iostream.h >
// Класс "радиус-вектор":
class vector
{ int N; // Размерность пространства
double *х; // Указатель на массив координат
// Прототип операции-функции:
friend vector& operator -(vector &);
public:
vector (int n, double *xn) // Конструктор
{ N = n; x = xn;}
// Компонентная функция печати вектора:
void display ();
};
// Определение компонентной функции:
void vector::display ()
{ cout << "\nКоординаты вектора:";
for (int i == 0; i < N; i++)
cout << "\t" << x[i] ;
}
// Определение операции-функции:
vector& operator - (vector & v)
{ for (int i = 0; i < v.N; i++) v.x(i] = -v.x[i]; return v;
} void main() // Иллюстрирующая программа
{ // Определяем массив:
double A[] = { 1.0, 2.0, 3.0, 4.0 };
vector V(4,A); // Создан объект класса vector
V. display(); // Вывод на экран
V = -V; // Перегруженная операция
V.display (); // Вывод на экран
}

Результат выполнения программы:

Координаты вектора: 1 2 3 4
Координаты вектора: -1 -2 -3 -4

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

.     прямой выбор компонента структурированного объекта;

.*     обращение к компоненту через указатель на него;

?:     условная операция;

::     операция указания области видимости;

sizeof    операция вычисления размера в байтах;

#     препроцессорная операция;

##     препроцессорная операция.

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

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

Нельзя изменить для перегруженных операций синтаксис выражений, т.е. невозможно ввести унарную операцию = или бинарную операцию ++.

Нельзя вводить новые лексические обозначения операций, даже формируя их из допустимых символов. Например, возведение в степень ** из языка Фортран нельзя ввести в языке Си++.

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

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

Любая унарная операция ' $ ' определяется для объектов некоторого класса также двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром.

Для префиксной операции '$' выражение $&z означает вызов компонентной функции z. operator $() или вызов глобальной функции operator $(z).

Синтаксис языка Си++ определяет некоторые встроенные операции над стандартными типами как комбинации других встроенных операций над теми же операндами. Например, для переменной long a = о; выражение ++m означает в += 1, что в свою очередь означает выполнение выражения m = m + 1. Такие автоматические замены выражений не реализуются и не справедливы для перегруженных операций. Например, в общем случае определение operator *=() нельзя вывести из определений operator *() и operator =().

Нельзя изменить смысл выражения, если в него не входит объект класса, введенного пользователем. "В частности, нельзя определить операцию-функцию, действующую только на указатели" [2]. Невозможно для операнда а типа int изменить смысл выражения 2 + m и т.п.

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

При разборе выражения аа + 2 компилятором выполняется вызов операции-функции аа. operator +(2) или operator +(aa,2).

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

При расширении действия операций приходится предусматривать всевозможные сочетания типов операндов. Например, определяя операцию сложения '+' для комплексных чисел, приходится учитывать сложение комплексного числа с вещественным и вещественного с комплексным, комплексного с целым и целого с комплексным и т.д. Если учесть, что вещественные числа представлены несколькими типами (float, double, long double) и целые числа имеют разные типы (int, long, unsigned, char), то оказывается необходимым ввести большое количество операций-функций. К счастью, при вызове операций-функций действуют все соглашения о преобразованиях стандартных типов параметров, и нет необходимости учитывать сочетания всех типов. В ряде случаев для бинарной операции достаточно определить только три варианта:

Например, для рассмотренного класса complex можно ввести как дружественные такие операции-функции:

complex operator + (complex x, complex у)
{ return(complex(x.real + у.real, x.imag + у.imag));}
complex operator + (double x, complex y)
{ return(complex(x + y.real, y.imag));}
complex operator + (complex x, double y)
{ return(complex(x.real + y, x.imag));}

После этого станут допустимыми выражения в следующих операторах:

complex СС (1.0,2.0);
complex ЕЕ;
ЕЕ. = 4.0 + СС;
ЕЕ =ЕЕ + 2.0;
ЕЕ = СС + ЕЕ;
ЕЕ = СС + 20; //По умолчанию приведение int к double
СС = ЕЕ + ' е '; //По умолчанию приведение char к double

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

complex(double x)
{ real = x; imag =0.0; }

позволяет удалить все дополнительные операции-функции, оставив только одну с прототипом:

friend complex operator +(complex, complex);

В этом случае целый операнд выражения 6 + ЕЕ автоматически преобразуется к типу double, а затем конструктор формирует комплексное число с нулевой мнимой частью. Далее выполняется операция-функция

operator +(complex(double(6), double(0)), ЕЕ)

Вместо включения в класс дополнительного конструктора с одним аргументом можно в заголовке единственного конструктора ввести умалчиваемое значение второго параметра:

complex(double re, double im = 0.0)
{ real re; imag = in; }

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

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

complex(double re, double im = 10.0);

при выполнении

complex LL = complex(1.0,2.0); LL = LL + 4 + 5;

получим LL = (10.0, 22.0), так как два неявных обращения к конструктору в выражении LL + 4 + 5 приводят к двум imag =10.

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

friend pair& operator ++(pair &);

Операция - перегружена с помощью компонентной операции-функции класса pair, не имеющей параметров:

pairC pair :: operator -- ();

В компиляторе ТС++ реализованы первые варианты языка Си++, и поэтому он не различает постфиксного и префиксного применений операций ++, --. Текст программы:

//Р9-13.СРР - перегрузка унарных операций ++, --
#include < iostream.h > // Класс "пара чисел":
class pair
{ int N; // Целое число
double x; // Вещественное число
// Дружественная функция:
friend pair& operator ++(pair &);
public:
pair (int n, double xn) // Конструктор
{ N = n; x = xn; }
void display ()
{ cout << "ХnКоординаты: N = " << N << "\tx = " << x;
}
pair& operator --() // Компонентная функция
{ N -= 1; x -= 1.0;
return *this;
}
};
pair& operator ++(pair& P) // Дружественная функция
{ P.N += 1; Р.х += 1.0;
return P;
}
void main()
{ pair Z(10,20.0);
Z. display ();
++Z;
Z.display ();
--Z;
Z. display ();
Z++;
Z.display();
Z--;
Z.display ();
}

Результат выполнения программы на ТС++:

Координаты: N = 10 х = 20
Координаты: N = 11 х = 21
Координаты: N = 10 х = 20
Координаты: N = 11 x = 21
Координаты: N = 10 х = 20

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

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

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

В следующей программе иллюстрируются возможности применения разных операций-функций для постфиксной и префиксной операций ++ и --:

//Р9-14.СРР - необычная перегрузка унарных операций ++, --
#include < iostream.h > // Класс "пара чисел":
class pair
{ int N; // Целое число
double x; // Вещественное число
// Дружественная функция для префиксной операции:
friend pair& operator ++(pairf&);
// Дружественная функция для постфиксной операции:
friend pair& operator ++(pairf&,int);
public:
pair (int n, double xn) // Конструктор
{ N = n; x = xn; }
void display()
{ cout << "\nКоординаты: N = " << N << " x = " << x;
}
// Компонентная функция (префиксная --):
pair& operator --()
{ N /= 10; x /= 10;
return *this;
}
// Компонентная функция (постфиксная --):
pair& operator --(int k)
{ M /= 2; x /= 2.0;
return *this;
}
};
pair& operator ++(pair& P) // Префиксная операция ++
{ P.N *= 10; P.x *== 10;
return P;
}
// Постфиксная операция ++:
pair& operator ++(pair& P,int k)
{ P.N = P.M * 2 + k;
P.x = P.x * 2 + k;
return P;
}
void main()
{ pair Z(10,20.0);
Z. display ();
++Z;
Z. display ();
--Z;
Z.display();
Z++;
Z.display();
Z--;
Z.display();
}

Результаты выполнения программы:

Координаты: N= 10 x = 20
Координаты: N = 100 x = 200
Координаты: N = 10 x = 20
Координаты: N = 20 x = 40
Координаты: N = 10 x = 20

Для демонстрации полной независимости смысла перегруженной операции от ее традиционного (стандартного) значения в операциях-функциях для префиксных операций ++ соответствуют увеличению в 10 раз, а -- уменьшению в 10 раз. Для постфиксных операций ++ определили как увеличение в 2 раза, а -- как уменьшение в 2 раза. Попытки использовать в постфиксных операциях-функциях значение дополнительного параметра int k подтверждает его равенство 0.

Глава 10. НАСЛЕДОВАНИЕ И ДРУГИЕ ВОЗМОЖНОСТИ КЛАССОВ

10.1. Наследование классов

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

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

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

Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, - производными (порожденными), иногда классами-потомками или наследниками. Производные классы "получают наследство" -данные и методы своих базовых классов - и, кроме того, могут пополняться собственными компонентами (данными и собственными методами). Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе (рис. 10.1).

Рис. 10.1. Схема обработки сообщений в иерархии объектов:
1 - обработка сообщения методами производного класса;
2 - обработка сообщения методами базового класса.

Если класс "точка (позиция) на экране" считать базовым классом, то на его основе можно построить класс "окно на экране". Данными этого класса будут две точки:

Методы класса "окно на экране":

Конструктор окна на экране:

Деструктор окна на экране:

Обратите внимание, что две точки по-разному используются в классе "окно на экране". Первая из них - это абсолютные координаты точки на экране, вторая - интерпретируется просто как пара чисел, определяющая размеры окна. Таким образом, если первая точка имеет координаты (4,3), а вторая (0,0), то это соответствует пустому окну (окну с нулевыми размерами). Наименьшее окно, в которое можно вывести один символ (или один пиксель в графическом режиме), должно иметь размеры (1,1) независимо от положения левого верхнего угла.

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

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

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

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

В иерархии классов соглашение относительно доступности компонентов класса следующее.

Собственные (private) методы и данные доступны только внутри того класса, где они определены.

Защищенные (protected) компоненты доступны внутри класса, в котором они определены, и дополнительно доступны во всех производных классах.

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

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

Еще раз отметим, что на доступность компонентов класса влияет не только явное использование спецификаторов доступа (служебных слов) - private (собственный), protected (защищенный), public (общедоступный), но и выбор ключевого слова class, struct, union, с помощью которого объявлен класс.

Определение производного класса. В определении и описании производного класса приводится список базовых классов, из которых он непосредственно наследует данные и методы. Между именем вводимого (нового) класса и списком базовых классов помещается двоеточие. Например, при таком определении

class S: X, Y, Z {...};

класс s порожден классами X,Y,Z, откуда он наследует компоненты. Наследование компонента не выполняется, если его имя будет использовано в качестве имени компонента в определении производного класса s. Как уже говорилось, по умолчанию из базовых классов наследуются методы и данные со спецификаторами доступа - public (общедоступные) и protected (защищенные).

В порожденном классе эти унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключевого слова class, и статус доступа public, если новый класс определен как структура, т.е. с помощью ключевого слова struct. Таким образом, при определении класса struct J: x, z {...}; любые наследуемые компоненты классов x, z будут иметь в классе J статус общедоступных (public). Пример:

class В { protected: int t;
public: char u;
};
class E: В { ... }; // t, u наследуются как private
struct S: В { ... }; // t, u наследуются как public

Таблица 10.1

Статусы доступа при наследовании
Доступ в базовом классе Спецификатор доступа перед базовым классом Доступ в производном классе
struct class
Public отсутствует public private
Protected отсутствует public private
Private отсутствует недоступны недоступны
Public public public public
Protected public protected protected
Private public недоступны недоступны
Public protected protected protected
Protected protected protected protected
Private protected недоступны недоступны
Public private private private
Protected private private private
Private private недоступны недоступны

ражения нет на экране, vis == 1 - изображение есть на экране); признак сохранения образа в оперативной памяти (tag == 0 - битовый образ не хранится, tag == 1 - битовый образ хранится в памяти); указатель pspot на область памяти, выделенную для хранения битового образа изображения.

// SPOT.CPP - класс, наследующий данные и методы
// класса POINT
#ifndef SPOT
#define SPOT 1
#include "point.cpp" // Определение класса point
class spot:
// 'public' позволит сохранить статусы доступа для
// наследуемых компонентов класса POINT:
public point
{ // Статус доступности данных в производных классах:
protected:
int rad // Радиус пятна (изображения)
int vis // Видимость пятна на экране
int tag // Признак сохранения образа в памяти
void *pspot; // Указатель на область памяти для
// изображения (для битового образа)
public:
// Конструктор класса SPOT:
spot(int xi, int yi, int ri):
// Вызов конструктора базового класса:
point(xi.yi)
{ int size;
vis =0; tag =0; rad = ri;
// Определить размеры битового образа:
size = imagesize(xi-ri,yi-ri,xi+ri,yi+ri);
// Выделить память для битового образа:
pspot = new char[size];
}
~spot() // Деструктор класса SPOT
{ hide(); // Убрать с экрана изображение пятна
tag = 0; // Сбросить признак сохранения в памяти
delete pspot; // Освободить память, где находился
// битовый образ
}
void show() // Изобразить пятно на экране дисплея
{ // Если битового образа нет в памяти:
if (tag == 0)
{ // Нарисовать окружность на экране:
circle(х,у,rad);
// Закрасить пятно floodfill(x,y,getcolor());
// Запомнить битовый образ в памяти:
getimage(x-rad,y-rad,x+rad,y+rad,pspot);
tag = 1;
}
else
// Перенести изображение из памяти на экран:
putimage(x-rad,y-rad,pspot,XOR_POT);
vis = 1;
}
void hide() // Убрать с экрана изображение пятна
{ if (vis == 0) // Нечего убирать
return;
// Стереть изображение с экрана:
putimage(x-rad,y-rad,pspot,XOR_POT);
vis = 0;
}
// Переместить изображение:
void move{int xn, int yn}
{ hide(); // Убрать старое изображение с экрана
// Изменить координаты центра пятна:
х = xn; у = yn;
show(); // Вывести изображение в новом месте
}
// Изменить размер изображения пятна:
void vary(float dr)
{ float a;
int size;
hide(); // Убрать старое изображение с экрана
tag = 0;
// Освободить память битового образа:
delete pspot;
// Вычислить новый радиус:
а = dr * rad;
if (а <= 0) rad = 0;
else rad = (int)a;
// Определить размеры битового образа:
size = imagesize(x-rad,y-rad,x+rad,y+rad);
// Выделить память для нового образа:
new char[size];
show(); // Изобразить пятно на экране 1
int& giver(void) // Доступ к радиусу пятна
{ return rad; }
};
#endif

В классе spot явно определены конструктор, деструктор ~spot () и пять методов:

show ()      вывести на экран изображение пятна, затем перенести его битовый образ в память;

hide ()      убрать с экрана изображение пятна;

move ()       переместить изображение в другое место на экране;

vary ()       изменить (уменьшить или увеличить) изображение на экране;

giver ()      обеспечить доступ к радиусу пятна.

Из класса point класс spot наследует координаты (х, у) точки (центра пятна) и методы givex(), givey(). Методы point::show(), point:: move () заменены в классе spot новыми функциями с такими же именами, а функция point::hide () не наследуется, так как в классе point она имеет статус собственного компонента (private).

Конструктор spot () имеет три параметра - координаты центра (xi, yi) и радиус пятна на экране (ri). При создании объекта класса spot вначале вызывается конструктор класса point, который по значениям фактических параметров, соответствующих xi, yi, определяет точку - центр пятна. Эта точка создается как безымянный объект класса point. (Конструктор базового класса всегда вызывается и выполняется до конструктора производного класса.) Затем выполняются операторы конструктора spot (). Здесь устанавливаются начальные значения признаков vis, tag, и по значению фактического параметра, соответствующего формальному параметру ri, определяется радиус пятна rad. С помощью стандартной функции imagesize () из графической библиотеки GRAPHICS. LIB вычисляется объем памяти (вспомогательная переменная size), требуемый для сохранения прямоугольного (квадратного) участка экрана, на котором предполагается изобразить пятно. Выделение участка основной памяти нужного объема выполняет стандартная операция new, операнд которой - это массив типа char из size элементов.

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

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

circle(x,y,rad)

нарисовать окружность с центром в точке с координатами (х, у) и радиусом rad;

floodfill(x,y,c)

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

getcolor ()

определить текущий цвет изображений;

gatimage (x1, y1, x2, y2, pnt)

поместить в заранее выделенный участок основной памяти, связанный с указателем pnt, битовый образ прямоугольного участка экрана, выделенного координатами левого верхнего (xl, yl) и правого нижнего (х2, у2) углов;

putimage(xl,yl,pnt,op)

изобразить на экране битовый образ, ранее сохраненный в памяти с помощью функции getimage (); xl, yl - координаты размещения на экране левого верхнего угла, pnt - указатель на область памяти, где хранится нужное изображение; ор -параметр, определяющий правила выбора цвета для каждого изображаемого пикселя. Выбор цвета осуществляется с учетом имеющегося на экране пикселя и сохраненного в памяти. Параметр ор определяет правило сочетания этих цветов в соответствии с табл. 10.2. Необходимо обратить внимание на одну особенность режима XOR_PUT. Если в этом режиме изображение вывести на экран в то же место, где уже было то же самое изображение, то изображение исчезнет с экрана. Именно так убирает с экрана пятны функция hide (). Флажок видимости пятна на экране vis необходим для распознавания необходимости повторного применения функции putimage ().

Функция show () выполняется в разных режимах в зависимости от значения признака tag записи изображения в память. Если значение tag равно 0, то рисуется и закрашивается окружность, затем ее образ переписывается в память функцией getimage () и устанавливается в 1 значение tag. Если значение tag равно 1, то образ переносится на экран из той области основной памяти, где он сохранялся при помощи функции putimage ().

Действия функции move() понятны из комментариев в тексте программы.

Таблица 10.2

Правила выбора цвета при размещении на экране битового образа с помощью ФУНКЦИИ putimage ()

Значение параметра ОР Условное обозначение в graphics.h Смысл преобразования
0 COPY_ PUT Копия без всяких условий
1 XOR_ PUT Исключающее ИЛИ
2 OR_ PUT Включающее ИЛИ (дизъюнкция)
3 AND_ PUT Логическое И (конъюнкция)
4 NOT_ PUT Копия с инверсией изображения

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

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

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

~имя_класса

У деструктора не может быть параметров (даже типа void), и деструктор не имеет возможности возвращать какой-либо результат, даже типа void. Статус доступа деструктора по умолчанию public (т.е. деструктор доступен во всей области действия определения класса).

В несложных классах деструктор обычно определяется по умолчанию. Например, в классе point деструктор явно не определен, и компилятор предполагает, что он имеет вид

~point() { };

В классе spot деструктор явно определен:

~spot()    {    hide();    tag = 0;    delete []    pspot;    }

Его действия: убрать с экрана изображение пятна, обратившись к функции spot:: hide (); установить в нуль признак tag наличия в памяти битового образа пятна; освободить память, выделенную при создании объекта для битового образа пятна и связанную с конкретным экземпляром указателя pspot.

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

public:    ~spot()    {    ~point();    }

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

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

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

Объяснив основные принципы работы деструкторов, приведем программу для работы с объектами класса spot:

//Р10-01.СРР - наследование классов и их деструкторы
#include < graphica.h > // Связь с графической библиотекой
#include < conio.h > // Прототип функции getch()
#include "spot.cpp" // Определение класса spot
void main()
{// Переменные для инициализации графики:
int dr = DETECT, mod;
II Инициализация графической системы:
initgraph(for,&mod,"с:\\borlandc\\bgi" );
{ // В этом блоке создаются и используются объекты
// класса spot
spot A(200,50,20); // Создается невидимое пятно А
spot 0(500,200,30); // Создается невидимое пятно D
A.show(); // Изобразить пятно А на экране
getch(); // Ждать нажатия клавиши
D.show(); // Изобразить пятно D на экране
getch(); A.move(50,60); // Переместить пятно А
getch(); D.vary(3) ; // Изменить размеры пятна D
getch(); // Ждать нажатия клавиши
}
// При выходе из блока для каждого объекта автоматически
// вызывается деструктор, освобождающий выделенную
// память
closegraph(); // Закрыть графический режим
} // Конец программы

Изменение состояний экрана при выполнении программы иллюстрирует рис. 10.2.

Принципиальным отличием этой программы от приведенных выше программ Р9-04.СРР, Р9-11.СРР для работы с объектами класса point является наличие внутреннего блока, что связано с наличием в классе spot деструктора, при выполнении которого вызывается компонентная функция hide(), использующая функции графики. Эти функции могут выполняться только в графическом режиме, т.е. до выполнения функции closеgraph(). Если построить программу без внутреннего блока так же, как упомянутые программы с классом point, то деструктор по умолчанию будет вызываться только при окончании программы, когда графический режим уже закрыт и выполнение любых графических функций невозможно. Указанной ошибочной ситуации можно избежать двумя путями: либо вызывать деструктор явно для уничтожения объектов А и D, а потом закрывать графический режим, либо после инициализации графики ввести внутренний блок, в котором определены объекты А и D и при выходе из которого они уничтожаются, для чего деструктор дважды вызывается автоматически. Графический режим закрывается во внешнем блоке, когда объекты А, D уже уничтожены и обращения к деструктору -spot () не нужны. В программе реализовано второе решение.

Рис. 10.2. Последовательность изображений на экране при выполнении программы Р10-01. СРР

В качестве несложного упражнения можно удалить скобки ' {} ', выделяющие внутренний блок, и убедиться, что при выполнении измененной программы будет выдаваться сообщение об ошибке в графической системе:

BGI Error: Graphics not initialized (use 'initgraph')

Второе решение - явный вызов деструкторов без добавления вложенного блока:

...
getch(); D.vary(3); // Изменить размеры пятна D
getch(); // Ждать нажатия клавиши
A.spot::~spot() ; // Уничтожить объект А
gwtch(); // Ждать нажатия клавиши
D.spot::~spot(); // Уничтожить объект D
closegraph(); // Закрыть графический режим
} // Конец программы

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

10.2. Множественное наследование и виртуальные базовые классы

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

А (базовый класс - прямая база для B)

­

B (производный от А класс - прямая база для C)

­

C (производный класс - C прямой базой в и косвенной А)

Рис. 10.3. Прямое и косвенное наследование классов

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

class X1 { ... };
class X2 { ... };
class X3 { ... };
class Yl: public X1, public X2, public X3 { ... };

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

//CIRC.CPP - определение класса "окружность"
#include < graphics.h >
class circ
{ int xc, ус, // Координаты центра
rc; // Радиус окружности
public:
// Конструктор:
circ(int xi, int yi, int ri)
{ xc = xi; yc = yi; rc = ri; } // Изобразить окружность на экране:
void show()
{ circle(xc,yc,rc); } // Убрать с экрана изображение окружности:
void hide()
( int bk, cc;
bk = getbkcolor(); // Цвет фона
cc = getcolor(); // Цвет изображения
setcolor(bk); // Сменить цвет рисования
// Рисуем окружность цветом фона:
circle(xc,ус,rс);
// Восстановить цвет изображения:
setcolor(cc);
}
};
//SQUARE.CPP - определение класса "квадрат"
#include < graphica.h >
class square
{int xq, yq, // Координаты центра квадрата
lq; // Длина стороны квадрата
// Вспомогательная функция рисования:
void rissquare(void)
{ int d = lq/2;
line(xq-d,yq-d,xq+d,yq-d);
line(xq-d,yq+d,xq+d,yq+d);
line(xq-d,yq-d,xq-d,yq+d);
line(xq+d,yq-d,xq+d,yq+d);
}
public:
// Конструктор:
square(int xi,int yi,infc li)
{ xq - xi; yq = yi; lq = li;}
// Изобразить квадрат на экране:
void show()
{ rissquare();}
// Убрать с экрана изображение квадрата:
void hide()
{ int bk, cc;
bk = getbkoolor(); // Цвет фона
cc = getcolor(); // Цвет изображения
setcolor(bk); // Сменить цвет рисования
rissquare(); // Рисуем квадрат цветом фона
setcolor(cc); // Восстановить цвет изображения
}
};

В следующей программе на основе классов circ и square создан производный класс "окружность в квадрате" с именем circsqrt:

//Р10-02.СРР - окружность в квадрате - множественное
// наследование
#include < conio.h > // Для функции getch()
#include "square.cpp" // Определение класса "квадрат"
#include "circ.cpp" // Определение класса "окружность"
// Производный класс - "окружность, вписанная в квадрат";
// Класс circsqrt наследует только методы обоих базовых
// классов. В нем нет наследуемых данных.
class circsqrt : public circ, public square
{ public:
// Конструктор:
circsqrt(int xi, int yi, int ri):
circ(xi,yi,ri), // Явно называются конструкторы
square(xi,yi,2*ri) // базовых классов
{}
// Изобразить на экране окружность в квадрате:
void show(void)
{ circ::show(); square::show();}
// Убрать с экрана изображение:
void hide ()
( square::hide (); circ::hide();}
};
void main()
{int dr = DETECT, mod;
initgraph(&dr,&mod,"c:\\borlandc\\bgi");
circsqrt Al(100,100,60);
circsqrt F4(400,300,50);
Al.show(); getch ();
F4.show(); getch();
F4.hide (); getch();
Al.hide(); getch();
closegraph();
}

Определения базовых классов должны предшествовать их использованию в качестве базовых. Поэтому тексты из файлов square. срр и circ.cpp включены в начало программы, после чего описывается класс circsqrt. В производном классе circsqrt телом конструктора служит пустой оператор. Выполнение конструктора circsqrt () сводится к последовательному вызову конструкторов базовых классов. При этом за счет соответствующего выбора параметров центры обеих фигур (квадрата и окружности) совпадают. Кроме того, длина стороны квадрата выбирается равной удвоенному радиусу окружности (параметр 2 * ri), и тем самым окружность оказывается вписанной в квадрат.

В основной программе формируются два объекта A1, р4 класса circsqrt. Они последовательно выводятся на экран дисплея (рис. 10.4) и в обратном порядке убираются с экрана.

Рис. 10.4. Последовательность изображений на экране при выполнении программы Р10-02. СРР

При множественном наследовании никакой класс не может больше одного раза использоваться в качестве непосредственного базового. Однако класс может больше одного раза быть непрямым базовым классом:

class X { ...; f(); ... };
class У: public X { ... };
class Z: public X { ... };
class D: public Y, public Z { ... };

В данном примере класс х дважды опосредованно наследуется классом D. Особенно хорошо это видно в направленном ациклическом графе (НАГ):

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

В качестве содержательного примера с дублированием непрямого базового класса рассмотрим программу, в которой определен класс spotelli - круглое пятно, вписанное в эллипс. Класс spotelli непосредственно базируется на классах spot и ellips, каждый из которых базируется, в свою очередь, на классе point. Таким образом, point дважды входит в spotelli в качестве непрямого базового класса, т.е. дублируется. Класс point (точка на экране) уже рассматривался. Текст его определения находится в файле point. срр (см. п. 9.4). Определение производного от класса point класса spot (круглое пятно на экране) находится в файле spot. срр (см. п. 10.1). На базе класса point можно следующим образом определить класс "эллипс":

//ELLIPS.СРР - класс "эллипс"
#ifndef ELLIPS
#define ELLIPS 1
#include "point.срр" // Определение класса point
class ellips : public point
{ protected: int rx,ry; // Радиусы эллипса
public:
// Конструктор:
ellips (int xc, int yc, int rx, int ry):
point (xc,yc)
{ this->rx = rx; this->ry = ry;}
void show() // Изобразить на экране эллипс
{ellipse(x,y,0,360,rx,ry);
return;
}
// Убрать с экрана изображение эллипса:
void hide()
{ int cc, bk;
cc = getcolor ();
bk = getbkcolor();
setcolor(bk);
ellipse(x,y,0,360,rx,ry);
setcolox(cc);
}
};
#endif

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

В классе ellips конструктор предусматривает задание четырех параметров: координаты центра (хс, ус) и радиусы вдоль осей (rx, ху). Координаты хс, ус используются в качестве параметров при вызове конструктора базового класса point. Чтобы различить компоненты rx, rу класса ellips и обозначенные теми же идентификаторaми формальные параметры конструктора, используется указатель this. В классе ellipe две общедоступные функции show() - изобразить 1 эллипс на экране дисплея; hide() - убрать с экрана изображение эллипса. Текст программы:

//Р10-ОЗ.СРР - круглое пятно в эллипсе - множественное
// наследование с дублированием базовых
// классов (дублируется класс point)
#include "spot.cpp"
#include "ellips.cpp" // Производный класс - дважды косвенно наследующий
// класс point:
class spotelli: public spot, public ellips
{ // Вспомогательная функция:
int min(int value1, int value2)
{return ((valuel < value2)? valuel: value2);
}
public:
// Конструктор:
spotelli(int xi,int yi,int rx,int ry):
ellips(xi,yi,rx,ry),
spot(xi,yi,min(rx,ry))
{ } // Вывести изображение на экран дисплея:
void show()
{ soot::show ();
ellips::show();
}
void hide() // Убрать изображение с экрана дисплея
{ spot::hide();
ellips::hide();
}
};
#include < conio.h > // Для функции getch()
void main()
{ int dr = DETECT, mod;
initgraph(&dr,&mod, "c:\\borlandc\\bgi " );
{ spotelli Al(100,100,20,80);
spotelli F4(400,300,230,100);
Al.show(); getch();
F4.show(); getch();
F4.hide(); getch();
A1.hide();
} closegraph();
}

В классе ellips, в классе spot и в классе spotelli наследуются данные х, у класса point - координаты точки на экране. В классе point они определены как защищенные (protected) и сохраняют тот же статус доступа в производных классах, где определяют координаты центров - пятна (класс spot), эллипса (класс ellips) и эллипса с пятном (класс spotelli).

Класс spot мы уже разбирали.

Рис. 10.5. Последовательность изображений на экране при выполнении программы Р10-03. СРР

Конструктор класса spotelli не выполняет никаких дополнительных действий - последовательно вызываются конструкторы класса ellips и класса spot, причем центры создаваемых фигур совпадают, а в качестве радиуса пятна выбирается меньший из радиусов эллипса. Используемая в этом случае функция min() определена по умолчанию как встроенная (inline) собственная (private) функция класса spotelli.

Чтобы отличать одинаково обозначенные функции, унаследованные классом spotelli из классов spot и ellips, при вызове show () и hide () используются полные квалифицированные имена, в которых применена операция ':: '.

Функция main () не содержит ничего нового. Описаны два объекта Al, F4 класса spotelli, которые последовательно изображаются на экране и "стираются" с него.

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

class Х { ... f () ;...};
class Y: virtual public X { ... };
class Z: virtual public X { ... };
class D: public Y, public Z { ... };

Теперь класс D будет включать только один экземпляр х, доступ к которому равноправно имеют классы Y и Z.

Графически это очень наглядно:

Иллюстрацией сказанного может служить иерархия классов в следующей программе:

//Р10-04.СРР - множественное наследование с виртуальным
// базовым классом
#include < iostream.h >
class base // Первичный (основной) базовый класс
{ int jj; char cc; char vv[10];
public:
base(int j = 0, char с = '*')
{ jj = j;
cc = c;
}
};
class dbase: public virtual base
{ double dd;
public:
dbase
{double d = 0.0) : base()
{ dd = d;}
};
class fbase: public virtual base
{float ff;
public:
fbase(float f = 0.0): base()
{ ff = f; }
};
class top: public dbase, public fbase
{ long tt;
public:
top(long t = 0) : dbase(), fbase()
{ tt = t; }
};
void main()
{ cout << "\n0сновной базовый класс: sizeof(base) = " << sizeof(base);
cout << "\nНепосредственная база: sizeof(dbase) = " << sizeof(dbase);
cout << "\nНепосредственная база: sizeof(fbase) = " << sizeof(fbase);
cout << "\nПроизводный класс: sizeof(top) = " << sizeof(top);
}

Результаты выполнения программы:

Основной базовый класс: sizeof(base) = 13
Непосредственная база: sizeof(dbase) = 23
Непосредственная база: sizeof(fbase) = 19
Производный класс: sizeof(top) = 33

Основной базовый класс base в соответствии с размерами своих компонентов стандартных типов int и char [ll]имеет размер 13 байт. Создаваемые на его основе классы dbase и fbase занимают соответственно 23 и 19 байт. (В dbase входит переменная типа double, занимающая 8 байт, наследуется базовый класс base, для которого требуется 13 байт, и 2 байта нужны для связи в иерархии виртуальных классов.) Производный класс top включает: один экземпляр базового класса base (13 байт); данные и связи класса dbase (10 байт); данные и связи класса fbase (6 байт); компонент long tt (4 байта).

Если в той же программе убрать требование виртуальности (атрибут virtual) при наследовании base в классах dbase и fbase, то результаты будут такими:

Основной базовый класс: sizeof(base) = 13
Непосредственная база: sizeof(dbase) = 21
Непосредственная база: sizeof(fbase) = 17
Производный класс: sizeof(top) = 42

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

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

class Х { ... };
class Y: virtual public X { ... };
class Z: virtual public X { ... };
class В: virtual public X { ... };
class С: virtual public X { ... };
class E: public X { ... };
class D: public X { ... };
class A: public D, public B,
public Y, public Z,
public C, public E { ... };

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

Возможны и другие комбинации виртуальных и не виртуальных базовых классов. Например:

class ВВ { ... };
class АА: virtual public ВВ { ... };
class CC: virtual public ВВ { ... };
class DD: public АА, public CC, public virtual ВВ { ... };

Соответствующий НАГ имеет вид:

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

class X { public: int d; ... };
class Y { public: int d; ... };
class Z: public X, public Y,
{ public:
int d;
...
d = X::d + Y::d;
...
};

10.3. Виртуальные функции и абстрактные классы

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

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

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

Рассмотрим теперь, как ведут себя при наследовании не виртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.

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

//BASE.DIR - определения базового и производного классов
struct base
{
void fun (int i)
{ cout << "\nbase::i = " << i; }
};
struct dir: public base
{ void fun (int i)
{ cout << "\ndir::i = " << i; }
};

В данном случае внешне одинаковые функции void fun (int) определены в базовом классе bаsе и в производном классе dir.

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

base: :fun().

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

В программе, где определены и доступны оба класса base и dir, обращения к функциям fun() могут быть выполнены с помощью указателей на объекты соответствующих классов:

//Р10-05.СРР - одинаковые функции в базовом и производном
// классах
#tinclude < iostream.h >
#include "base.dir" // Определения классов
void main(void)
{ base B, *bp = &B;
dir D, *dp = &D;
base *pbd = &D;
bp->fun(1); // Печатает : base::i = 1
dp->fun(5); // Печатает : dir::i = 5
pbd->fun(4); // Печатает : base::i = 4
}

Результаты выполнения программы:

base::i = 1
dir::i = 5
base::i = 4

В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес объекта производного класса (объекта D) присваивается указателю на объект его прямого базового класса (base *). При этом выполняется стандартное преобразование указателей, предусмотренное синтаксисом языка Си++. Обратное преобразование, т.е. преобразование указателя на объект базового класса в указатель на объект производного класса, невозможно (запрещено синтаксисом). Обращения к функциям классов base и dir с помощью указателей Ьр и dp не представляют особого интереса. Вызов pbd->fun() требуется прокомментировать. Указатель pbd имеет тип base*, однако его значение - адрес объекта D класса dir.

Какая же из функций base::fun() или dir::fun() вызывается при обращении pbd->fun()? Результат выполнения программы показывает, что вызывается функция из базового класса. Именно такой вызов предусмотрен синтаксисом языка Си++, т.е. выбор функции (не виртуальной) зависит только от типа указателя, но не от его значения. "Настроив" указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса.

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

имя_производного_класса::show()

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

имя_ объекта_ производного класса.show()

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

Большую гибкость (особенно при использовании уже готовых библиотек классов) обеспечивает позднее (отложенное), или динамическое связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать спецификатор virtual. Прежде чем объяснить преимущества динамического связывания, приведем пример. Опишем в базовом классе виртуальную функцию и введем два производных класса, где определим функции такими же прототипами, но без спецификатора virtual. В следующей программе в базовом классе base определена виртуальная функция void vfun(int). В производных классах dirl, dir2 эта функция подменяется (override), т.е. определена по-другому:

//Р10-06.СРР - виртуальная функция в базовом классе
#include < iostream.h >
#include < conio.h >
struct base
{
virtual void vfun(int i)
{ cout << "\nbase::i = " << i; }
};
struct dirl: public base
{ void vfun (int i)
{ cout << "\ndirl::i = " << i; }
};
struct dir2: public base
{ void vfun (int i)
{ cout << "\ndir2::i = " << i; }
};
void main(void)
{ base B, *bp = &B;
dirl D1, *dp1 = &D1;
dir2 D2, *dp2 = &D2:
bp->vfun(1); // Печатает: base::i = 1
dpl->vfun(2); // Печатает: dirl::i = 2
dp2->vfun(3); // Печатает: dir2::i = 3
bp = &D1; bp->vfun(4); // Печатает: dirl::i = 4
bp = &D2; bp->vfun(5); // Печатает: dir2::i = 5
}

Результат выполнения программы:

base::i = 1
dirl::i = 2
dir2::i = 3
dir1::i = 4
dir2::i = 5

В примере надо обратить внимание на доступ к функциям vfun () через указатель bp на базовый класс. Когда bp принимает значение адреса объекта класса base, то вызывается функция из базового класса. Затем bp последовательно присваиваются значения ссылок на объекты производных классов &Dl, &D2, и выбор соответствующего экземпляра функции vfun () каждый раз определяется именно объектом. Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, т.е. от типа объекта, для которого выполняется вызов. В противоположность этому интерпретация вызова через указатель не виртуальной функции зависит только от типа указателя (это было показано в предыдущем примере с функцией fun ()).

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

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

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

Сказанное иллюстрирует следующая программа:

//Р10-07.СРР - некоторые особенности виртуальных функций
#include < iostream.h >
#include < conio.h >
struct base
{
virtual void f1(void)
{ cout << "\nbase::f1"; }
virtual void f2(void)
{ cout << "\nbase::f2"; }
virtual void f3(void)
{ cout << "\nbase::f3"; }
};
struct dir: public base
{
// Виртуальная функция:
void fl(void)
{ cout << "\ndir::fl" ; }
// Ошибка в типе функции:
// int f2(void)
{ cout << "\ndir::f2" ; } // Не виртуальная функция:
void f3(int i)
{ cout << "\ndir::f3::i = "<< i; }
};
void main(void)
{ base B, *pb = &B;
dir D, *pd = &D;
pb->fl();
pb->f2();
pb->f3();
pd->fl();
pd->f2();
// Ошибка при попытке без параметра вызвать
dir::f3(int)
// pd->f3();
pd->f3(0);
pb = &D;
pb->f1();
pb->f2();
pb->f3();
// Ошибочное употребление или параметра, или указателя:
// pb->f3(3);
}

Результат выполнения программы:

base :f1
base :f2
base :f3
dir: f1
base :f2
dir: f3::i = 0
dir: fl
base :f2
base :f3

Обратите внимание, что три виртуальные функции базового класса по-разному воспринимаются в производном классе, dir::fl() -виртуальная функция, подменяющая функцию base::fl(). Функция base::f2() наследуется в классе dir так же, как и функция base::f3(). Функция dir::f3 (int) - совершенно новая компонентная функция производного класса, никак не связанная с базовым классом. Именно поэтому невозможен вызов f3 (int) через указатель на базовый класс. Виртуальные функции base::f2() и base::f3() оказались не переопределенными в производном классе dir. Поэтому при всех вызовах без параметров f3() используется только компонентная функция базового класса. Функция dir::f3(int) иллюстрирует соглашение языка о том, что если у функции производного класса набор параметров отличается от набора параметров соответствующей виртуальной функции базового класса, то это не виртуальная функция, а новый метод производного класса.

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

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

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

struct base {
virtual int f(int j)
{ return j*j;}
};
struct dir: public base {
int f(int i)
{ return base::f(i * 2); }
};

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

virtual тип имя функции(список формальных параметров) = 0;

В этой записи конструкция "= 0" называется "чистый спецификатор". Пример описания чистой виртуальной функции:

virtual void fpure(void) = 0;

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

Предположим, что имеется абстрактный класс:

class В { protected:
virtual void func(char) = 0;
void sos(int);
};

На основе класса в можно по-разному построить производные классы:

class D: public В {
...
void func(char);
};
class E: public В {
void sos(int);
};

В классе D чистая виртуальная функция func () заменена конкретной виртуальной функцией того же типа. Функция B::sos() наследуется классом D и доступна в нем и в его методах. Класс D не абстрактный. В классе E переопределена функция B::sos(), а виртуальная функция в:: func () унаследована. Тем самым класс E становится абстрактным и может использоваться только как базовый.

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

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

Например, из абстрактного класса "фигура" можно сформировать класс "треугольник", "окружность" и т.д.

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

show ()

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

show ()

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

Определение абстрактного класса figure (в файле figure.срр):

//FIGORE.CPP - абстрактный класс на базе класса
#include "point.срр"
class figure: public point {
public:
// Конструктор абстрактного класса figure:
figure (point p) : point
(p. givex (), p.givey()) { } // Чистая виртуальная функция для будущего
// изображения фигур:
virtual void show() = 0;
// Функция для удаления изображения фигуры:
void hide()
{ int bk, cc;
bk = getbkcolor();
cc = getcolor();
setcolor(bk);
show(); // Обращение к чистой виртуальной функции
setcolor(cc);
}
void move(point p) // Перемещение фигуры в точку "p"
{ hide(); x = p.givex(); у = p.givey(); show() ; }
};

На базе класса figure определим неабстрактные классы:

//ELLIPS.FIG - конкретный класс "эллипс" на основе figure
class ellips : public figure {
int rx,ry;
public:
// Конструктор:
ellips (point d, int radx, int rady): figure(d)
{ rх = radx; ry = rady;}
void show()
{ ellipse(x,y,0,360,rx,ry);
return;
}
};
//CIRC.FIG - конкретный класс "окружность"
class circ: public figure {
int radius;
public:
// Конструктор:
circ(point e, int rad) : figure(e)
{ radius = rad; }
void show()
{ circle(x,y,radius);
}
};

В следующей программе используются все три класса:

//Р10-08.СРР - абстрактные классы и чистые виртуальные
// функции
#include < graphics.h >
#include "figure.cpp"
#include "circ.fig"
#include "ellips.fig"
#include < conio.h > // Для функции
getch()
void main(void)
{ point A(100,80), B(300,200);
circ С(A,60);
ellips E(B,200,100);
{ // Переменные для инициализации графики:
int dr = DETECT, mod;
// Инициализация графической системы:
initgraph(&dr, &mod,"c:\\borlandc\\bgi" );
// Изобразить точку - point::show():
A.show(); getch();
// Изобразить точку - point::show():
В.show(); getch();
// Показать окружность - circ::show():
C.show(); getch();
// Показать эллипс - ellips::show():
E.show(); getch();
// Совместить фигуры - circ::figure::move ():
С.move(B); getch();
// Убрать эллипс - ellips::figure::hide():
E.hide(); getch();
// Убрать окружность - circ::figure::hide ():
С.hide (); getch();
} closegraph();
}

Графические результаты выполнения показаны на рис. 10.6.

Рис. 10.6. Последовательность изображений на экране при выполнении программы Р10-08. CPP

В программе на базе класса figure определены два производных класса: circ (окружность) и ellips (эллипс). Для обоих классов унаследованный класс point определяет центры фигур. В обоих классах определены конкретные методы show () и из абстрактного класса figure унаследованы функции move () и hide (). Комментарии к операторам основной программы содержат полные (квалифицированные) имена исполняемых функций.

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

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

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

В класс chain входят (рис. 10.7) в качестве компонентов: статический указатель на последний элемент, уже включенный в список (last), статический указатель на начало списка (begin), указатель в объекте на следующий элемент списка (next), указатель в объекте на абстрактный базовый класс figure (pfig). Параметр конструктора класса chain пусть имеет тип указателя на абстрактный базовый класс figure. В качестве фактических параметров будем использовать ссылки на конкретные объекты классов, производных от абстрактного класса figure. Тем самым в односвязный список включаются (рис. 10.7) конкретные фигуры (окружность - класс circ, эллипс - класс ellips).

Рис 10.7. Схема односвязного списка (класс chain), объединяющего объекты разных классов.

Текст программы со всеми включениями и определениями:

//Р10-09.СРР - односвязный список объектов разных классов
#include < stdlib.h > // NULL, malloc,...
#include < conio.h > // getch(),... //
#include < iostream.h > // cout,...
#include "point.cpp" // Базовый класс для figure
// Абстрактный класс, производный от point:
#include "figure.cpp"
// Класс, производный от абстрактного figure:
#include "circ.fig"
// Класс, производный от абстрактного figure:
#include "ellips.fig"
// Объекты класса - фигуры, включенные в односвязный
// список:
class chain
{ // Объект - элемент в односвязном списке
// Указатель на последний элемент в списке:
static chain *last;
// Указатель в объекте на следующий элемент:
chain *next;
public:
// Указатель на фигуру, входящую в элемент списка:
figure *pfig;
// Указатель на начало списка:
static chain *begin;
// Конструктор:
chain(figure *p);
// Функция изображает все фигуры списка:
static void showAll(void);
}; // Конец определения класса
// Внешнее описание и инициализация статических
// компонентов класса:
chain *chain::begin = NULL; // Начало списка chain
*chain::last = NULL; // Последний элемент в списке
void chain::showAll(void) // Изображение элементов списка
{ chain *uk = begin; // Настройка на начало списка
while (uk != NULL) // Цикл до конца списка
{ uk->pfig->show(); // Нарисовать конкретную фигуру
uk = uk->next; // Настройка на следующий элемент
}
}
// Конструктор создает и включает в список объект,
// связав его с конкретной фигурой из класса, производного
//от абстрактного:
chain:: chain(figure *p) // р - адрес включаемой фигуры
{ if (begin == NULL) // Определили начало списка
begin = this;
else last->next = this; // Связь с предыдущим элементом
pfig = р; // Запоминаем адрес включаемой фигуры
next = NULL; // Пометим окончание списка
last = this; // Запоминаем адрес последнего элемента
// списка
}
void main()
{ point A(l00,80), В(300,200);
circ С (А,60);
ellips Е(В,200,100);
{ // Переменные для инициализации графики:
int dr = DETECT, mod;
// Инициализация графической системы:
initgraph(&dr,&Mod,"с:\\borlandc\\bgi");
A.show(); getch(); // Изобразить точку - point:: show ()
В.show(); getch(); // Изобразить точку
// Показать окружность - circ::show():
C.show(); getch();
// Включить в список первый элемент - окружность С:
chain са(&С);
E.show (); getch(); // Показать эллипс - ellips::show()
chain ca(&Z); // Включить в список эллипс
// Совместить фигуры - circ::figurе::move():
C.mov(B); getch();
// Убрать эллипс - ellips::figure::hide():
E.hide (); getch();
// Убрать окружность - circ::figure::hide():
C.hide(); getch();
// Изобразить все фигуры из списка:
chain::showAll();
getch();
}
closegraph();
}

Рис. 10.8. Последовательность изображений на экране при выполнении программы Р10-09. СРР

Статическая компонентная функция chain::showAll() обеспечивает вывод на экран изображений всех конкретных фигур, включенных в односвязный список. Важно отметить последовательность передач управления в этой функции. Указатель uk типа chain * позволяет обратиться к компоненту pfig - указателю на абстрактный базовый класс figure. После выполнения конструктора chain () значением pfig является ссылка на объект некоторого производного от figure класса. Именно оттуда выбирается соответствующая функция show (), подменяющая чистую виртуальную функцию figure:: show (). Тем самым на экран выводится изображение конкретной фигуры. Например, функция circ:: show () изобразит окружность и т.д.

В основной программе формируются точки А, B и на них, как на центрах, создаются объекты с (окружность) и Е (эллипс). В графическом режиме выводятся на экран и убираются с экрана изображения всех созданных объектов. Затем функцией showAll() рисуются все объекты, включенные в список. Результат выполнения программы показан на рис. 10.8.

10.4. Локальные классы

Класс может быть определен внутри блока, например внутри тела функции. Такой класс называется локальным. Локализация класса предполагает недоступность его компонентов вне области определения класса (вне тела функции или блока, в котором он описан или определен).

Локальный класс не может иметь статических данных, так как компоненты локального класса не могут быть определены вне текста класса.

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

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

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

//Р10-10.СРР - внешние, локальные и глобальные классы
#include < conio.h >
#include "point.cpp" // Внешний класс "точка"
class square // Глобальный класс "квадрат"
{ class segment // Локальный класс "отрезок"
{ point pn, pk; // Точки начала и конца отрезка
public:
// Конструктор отрезка
segment(point pin = point(0,0),
point pik = point(0,0))
{ pn.givex() = pin.givex()
pn.givey() = pin.givey()
pk.givex() = pik.givex()
pk.givey() = pik.givey()
}
// Доступ к граничным точкам отрезка:
point& beg(void)
{ return pn; }
point& end(void)
{ return pk; }
void showSeg() // Изобразить отрезок на экране
{ line (pn.givex(), pn.givey(),
pk.givex( ), pk.givey());
}
};
// Конец определения класса segment
segment ab, bc, cd, da; // Отрезки - стороны квадрата
public:
// Конструктор квадрата:
square (point ci = point(0,0), int di = 0)
{ // Вершимы квадрата - локальные объекты
// конструктора:
point a, b, с, d;
a.givex() = ci.givex() - di/2;
a.givey() = ci.givey() - di/2;
b.givex() = ci.givex() + di/2;
b.givey() = ci.givey() - di/2;
c.givex() = ci.givex() + di/2;
c.givey() = ci.givey() + di/2;
d.givex() = ci.givex() - di/2;
d.givey() = ci.givey() + di/2;
// Граничные точки отрезков:
ab.beg() = a; ab.end() = b;
bс.beg () = b; be.end () = с;
cd.beg() = с; cd.end() = d;
da.beg() = d; da.end() = a;
}
void showSquare(void) // Изобразить квадрат
{ ab. showSeg ();
bc.showSeg();
cd.showSeg();
da.showSeg();
}
};
// Конец определения класса "квадрат"
void main()
{ // Переменные для инициализации графики:
int dr = DETECT, mod;
// Инициализация графической системы:
initgraph(&dr,&mod,"с:\\borlandc\\bgi " );
point pi(80,120);
point р2(250,240);
square A(pi,30);
square B(p2,140);
А.showSquare(); getch ();
В.showSquare(); getch();
closegraph();
}

Результат в графическом виде показан на рис. 10.9.

Рис. 10.9. Изображения на экране при выполнении программы Р10-10 .СРР

Отметим некоторые особенности программы. Класс "квадрат" (square) включает в качестве данных четыре стороны - отрезки ab, bс, cd, da, каждый из которых есть объект локального класса segment. Конструктор класса square () по заданным центру квадрата и размеру стороны определяет значения точек-вершин, а уже по ним формирует отрезки - стороны квадрата.

10.5. Классы и шаблоны

Шаблоны, которые иногда называют родовыми или параметризованными типами, позволяют создавать (конструировать) емействас родственных функций и классов [2], [9].

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

template <список_ параметров_ шаблона> определение функции

Здесь угловые скобки являются неотъемлемым элементом определения. Список параметров шаблона должен быть заключен именно в угловые скобки.

Аналогично определяется шаблон семейства классов:

template <список_ параметров_ шаблона> определение_ класса

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

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

Следуя авторам языка и компилятора Си++ [2, 9], рассмотрим векторный класс (в число данных входит одномерный массив). Какой бы тип ни имели элементы массива (целый, вещественный, с двойной точностью и т.д.), в этом классе должны быть определены одни и те же базовые операции, например доступ к элементу по индексу и т.д. Если тип элементов вектора задавать как параметр шаблона класса, то система будет формировать вектор нужного типа (и соответствующий класс) при каждом определении конкретного объекта.

Следующий шаблон позволяет автоматически формировать классы векторов с указанными свойствами:

//TEMPLATE.VEС - шаблон векторов
template < class T > // Т - параметр шаблона
class Vector
{ Т *data; // Начало одномерного массива
int size; // Количество элементов в массиве
public:
Vector(int); // Конструктор класса
vector ~Vector()
{ delete[ ] data;
} // Деструктор
// Расширение действия (перегрузка) операции "[]":
T& operator[] (int i)
{ return data(i]; }
};
// Внешнее определение конструктора класса:
template < class T >
Vector <Т>::Vector(int n)
{ data = new T[n];
size = n;
};

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

имя_ параметривованного_класса
<фактические_параметры_шаблона>
имя_ объекта (параметры_конструктора);

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

Vector < double > Z(8);

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

//Р10-11.СРР - формирование классов с помощью шаблона
#include "template.vec" // Шаблон классов "вектор"
#include < iostream.h >
main()
{ // Создаем объект класса "целочисленный вектор":
Vector < int > Х(5);
// Создаем объект класса "символьный вектор":
Vector < char > С(5);
// Определяем компоненты векторов:
for (int i = О; i < 5; i++)
{ X[i] = i; C[i] = 'A' + i;}
for (i = 0; i < 5 ; i++)
cout << " " << X[i] << ' ' << C[i];
}

Результат выполнения программы:

ОА     1В     2 С     3D     4Е

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

В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее - это параметры, для которых тип фиксирован:

//Р10-12.СРР
#include < iostream.h >
template <сlаss Т, int size = 64>
class row
{T *data;
int length;
public: row()
{ length = size;
data = new T[sixe];
}
~row()
{ delete[ ] data; }
T& operator [] (int i)
{ return data[i]; }
};
void main()
{ row < float,8 > rf;
row < int,8 > ri;
for (int i = 0; i < 8; i++) { rf[i] = i; ri[i] = i * i;}
for (i = 0; i < 8; i++)
cout << " " << rf[i] << ' ' << ri[i];
}

Результат выполнения программы:

0  0     1  1     2  4     3  9     4  16     5  25     6  36     7  49

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

Глава 11. ВВОД-ВЫВОД В ЯЗЫКЕ СИ++

11.1. Общие сведения о библиотеке потокового ввода-вывода

Под "программированием на языке Си++" обычно понимается "программирование в среде Си++". Дело в том, что никакая полезная программа не может быть написана на языке Си++ без привлечения библиотек, включаемых в конкретную среду (в компилятор) языка. Конкретная среда Си++, в которой разрабатывается программа, обычно обеспечивает программиста удобными средствами для работы с ее библиотеками. При этом по утверждению Б.Страуструпа: "Для использования библиотеки совсем не нужно знание методов, которые применялись для ее реализации". Однако знание правил использования средств библиотеки совершенно необходимо. Самая незаменимая из этих библиотек - библиотека ввода-вывода, так как средства ввода-вывода непосредственно в язык Си++ (также как и в язык Си) не входят. В программах на языке Си++ можно равноправно использовать две библиотеки ввода-вывода: стандартную библиотеку функции языка Си (стандарт ANSI С) и библиотеку классов, специально созданную для языка Си++. Библиотека функции языка Си становится доступной в программе, как только в ее заголовок будет включен файл stdio.h. Для обращения к функциям требуются сведения об их прототипах и соблюдение соглашений стандарта (см. прил. 3). Подробную информацию можно получить из технической документации того компилятора, с которым вы работаете.

На протяжении всей книги в программах постоянно использовалась препроцессорная директива:

#include < iostream.h >

Назначение указанного в директиве заголовочного файла iostream.h - связать компилируемую программу с одной из основных частей библиотеки ввода-вывода, построенной на основе механизма классов. Эта библиотека ввода-вывода почти стандартна, так как включена практически во все компиляторы Си++. Однако о стандарте библиотеки ввода-вывода Си++ можно говорить только неформально. Библиотека создана позже, чем появился язык, она разрабатывалась в некотором смысле независимо от создания языка Си++, не входит в формальное описание языка и написана на языке Си++.

Потоки ввода-вывода. В соответствии с названием заголовочного файла iostream.h (stream - поток; "i" - сокращение от input - ввод;

"о" - сокращение от output - вывод) описанные в этом файле средства ввода-вывода обеспечивают программиста механизмами для извлечения данных из потоков и для включения (внесения) данных в потоки. Поток определяется как последовательность байтов (символов) и с точки зрения программы не зависит от тех конкретных устройств (файл на диске, принтер, клавиатура, дисплей, стример и т.п.), с которыми ведется обмен данными. При обмене с потоком часто используется вспомогательный участок основной памяти - буфер потока (рис. 11. 1 - буфер вывода, рис. 11.2 - буфер ввода).

Рис. 11.1. Буферизированный выходной поток

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

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

Рис. 11.2. Буферизированный входной поток

Используемые в программах потоки логически делятся на три типа:

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

В соответствии с особенностями "устройства", к которому "присоединен" поток, потоки принято делить на стандартные, консольные, строковые и файловые.

Из перечисленных потоков мы не будем рассматривать только консольные потоки. Причин для этого несколько. Во-первых, консольные потоки отсутствовали в классических реализациях библиотеки потокового ввода-вывода [26]. Во-вторых, консольные потоки несовместимы с операционной средой Microsoft Windows и могут использоваться только при разработке программ, работающих под управлением MS-DOS (см., например, [9, 21, 29, 30, 31]). Консольные потоки поддерживаются классом constream и обеспечивают удобный доступ к терминалу. В них есть возможности работы с клавиатурой и средства манипуляции с участками экрана и с экраном в целом.

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

Однако предварительно остановимся на некоторых принципиальных особенностях потоковой библиотеки ввода-вывода языка Си++.

Иерархия классов библиотеки ввода-вывода. В отличие от стандартной библиотеки (в которой находятся средства, например, для работы со строками, или математические функции), унаследованной компиляторами языка Си++ от языка Си, библиотека ввода-вывода Си++ является не библиотекой функций, а библиотекой классов. Это первая "промышленная" библиотека классов, разработанная для распространения совместно с компиляторами. Именно эту библиотеку рекомендуют изучать, начиная знакомиться с принципами объектно-ориентированного программирования [28]. Одним из базовых принципов ООП является предположение о том, что объекты "знают", что нужно делать при появлении обращения (сообщения) определенного типа, т.е. для каждого типа адресованного ему обращения объект имеет соответствующий механизм обработки. Если мы используем объект cout, представляющий выходной поток, то как уже неоднократно показано на примерах, для каждого из базовых типов (nt, long, double, ...i) этот объект cout выбирает соответствующую процедуру обработки и выводит значение в соответствующем виде. Объект cout не может перепутать и вывести, например, целое число в формате с плавающей точкой. От таких ошибок, которые были возможны в языках Си или Фортран, когда программист сам определял форму внешнего представления, библиотека классов ввода-вывода хорошо защищена.

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

При работе с потоковой библиотекой ввода-вывода программист обычно достаточно активно использует следующие классы:

tos - базовый потоковый класс;
istream - класс входных потоков;
ostream - класс выходных потоков;
iostream - класс двунаправленных потоков ввода-вывода;
istrstream - класс входных строковых потоков;
ostrstream - класс выходных строковых потоков;
strstream - класс двунаправленных строковых потоков (ввода-вывода);
if stream - класс входных файловых потоков;
of stream - класс выходных файловых потоков;
fstream - класс двунаправленных файловых потоков (ввода-вывода);
constream - класс консольных выходных потоков.

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

Рис. 11.3. Упрощенная схема иерархии потоковых классов

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

Потоковые классы, их данные и методы становятся видимыми и доступными в программе, если в нее включен нужный заголовочный файл:

iostream.h - для классов ios, istream, ostream, stream;
strstrea.h - для классов istrstream, ostrstream, strstream;
fstream. h - для классов if stream, of stream, f stream;
constrea. h - для класса constream.

Так как класс tos является базовым для остальных потоковых классов, то включение в текст программы любого из заголовочных файлов strstrea.h, constrea.h или fstream.h автоматически подключает к программе файл iostream.h. Соответствующие проверки выполняются на этапе препроцессорной обработки.

В заключение перечислим отличительные особенности применения механизма потоков. Потоки обеспечивают:

11.2. Стандартные потоки для базовых типов

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

#include < iostream.h >

и можно с помощью операций включения (записи) данных в поток << и извлечения (чтения) данных из потока >> выполнять обмен с дисплеем и клавиатурой ЭВМ.

Объясняется это тем, что заголовочный файл iostream.h не только подключает к программе описания классов ios, istream, ostream, stream, но и содержит определения стандартных потоков ввода-вывода:

cin - объект класса istream, связанный со стандартным буферизированным входным потоком (обычно клавиатура консоли);

cout - объект класса ostream, связанный со стандартным буферизированным выходным потоком (обычно дисплей консоли);

cerr - объект класса ostream, связанный со стандартным небуферизированньм выходным потоком (обычно дисплей консоли), в который направляются сообщения об ошибках;

clog - объект класса ostream, связанный со стандартным буферизированным выходным потоком (обычно дисплей консоли), в который с буферизацией направляются сообщения об ошибках.

Каждый раз при включении в программу файла iosfcream.h происходит формирование объектов cin, cout, cerr, clog, т.е. создаются соответствующие стандартные потоки, и программисту становятся доступными связанные с ними средства ввода-вывода. Программист может по своему усмотрению разорвать связь любого из перечисленных объектов с консолью и соединить его с тем или иным файлом, но стандартная (по умолчанию) связь устанавливается именно с клавиатурой (поток cin) и дисплеем (потоки cout, cerr, clog). В том же файле iostream.h, где описаны классы istream, ostream, для них определены оригинальные операции ввода и вывода данных. Операция ввода класса istceam называется извлечением (чтением) данных из потока. Она обозначается с помощью символа операции сдвига вправо >>. Операция вывода класса ostream называется вставкой или включением (или записью) данных в поток. Она обозначается с помощью символа операции сдвига влево <<. Роль операции извлечения и вставки конструкции << и >> играют по умолчанию только в том случае, если слева от них находятся объекты, соответственно, классов iostream и ostream:

cin >> имя_объекта_ базового_ типа
cout << выражение_базового_ типа
cerr << выражение_баэового_типа
clog << выражение_базового_типа

Выполнение операции >> (извлечение из потока) заключается в преобразовании последовательности символов потока в значение типизированного объекта, частным случаем которого является переменная базового типа int, long, double и т.д. При выполнении операции << (включение в поток) осуществляется обратное преобразование - типизированное значение выражения (int, float, char и т.д.) трансформируется в последовательность символов потока. Примеры применения операций включения в поток и извлечения из потока типизированных значений уже приводились многократно. Хорошо бы теперь читателю удивиться некоторым особенностям ввода-вывода с помощью этих операций.

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

Например, во внутреннем коде целое число может быть представлено двумя смежными байтами. Те же самые смежные байты можно рассматривать как внутренние коды двух литер (символов). (Именно действует объединение union в языке Си++, позволяя по-разному интерпретировать внутренние коды данных.)

Средства вывода, применяемые в языке, должны иметь возможность распознать тип выводимых данных и в указанном примере поместить в выходной поток либо код внешнего представления целого числа, либо два кода расположенных рядом символов. Для обеспечения такой возможности операция << включения в стандартный выходной поток перегружена. Существуют ее варианты для типов char, unsigned short, signed short, signed int, unsigned int, signed long, unsigned long, float double, long double, char *, void *. Все они доступны после включения в программу файла iostream.h. Отметим, что операция включения определена только для указателей двух типов. Этого вполне достаточно, так как все указатели, отличные от char *, автоматически приводятся к типу void *.

Предопределенный обмен данными со стандартными потоками.

Рассмотрим процесс ввода данных с применением операции извлечения. Например, как нужно в программе воспринять последовательность -2.3е+1, набираемую на клавиатуре? Это символьная строка, которую нужно разместить в массиве типа char[], или экспоненциальное представление вещественного числа типа float, либо типа double? Набирая на клавиатуре последовательность цифр 1234, можно интерпретировать ее либо как целое число, либо как символьную строку, либо как значение вещественного числа. А как же правильно ее воспринять?

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

cout << "\n1234 << " << 1234;

выведет с новой строки последовательность символов 1234 =, а затем целое число со значением 1234. И никакой подсказки от программиста не требуется! На основе анализа типа выражения, помещенного справа от операции включения <<, в поток помещаются коды внешнего представления данных соответствующих типов.

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

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

//Р11-01.СРР - "стандартная" перегрузка операций <<, >>
// для базовых типов
#include < iostream.h >
void main()
{ union { long integer;
char line[4];
float real;
} mix;
cout << "\n\nВведите целое число (mix.integer):";
cin >> miх. integer;
cout << "Mix.integer=" << mix.integer;
cout << "\nmix.line=" << mix.line;
oout << "\nmix.real=" << mix.real;
cout << "\n\nВведите строку (mix.line):";
cin >> mix. line;
cout << "mix.integer = " << mix.integer;
cout << "\nmix.line = " << mix.line;
cout << "\nmix.real = " << mix.real;
cout << "\n\nВведите вещественное число (mix.real):";
cin >> mix. real;
cout << "mix. integer = " << mix.integer;
cout << "\nmix.line = " << mix.line;
cout << "\nmix.real = " << mix.real;
}

Результат выполнения программы:

Введите целое число (mix.integer): 888 < Enter >
0 mix.integer = 888
mix.line = xv
mix.real = 1.244353e-42
Введите строку (mix.line): 888 < Enter >
mix. integer = 3684408
mix.line = 888
mix.real = 5.162955e-39
Введите вещественное число (mix.real): 888 < Enter >
miх.integer = 1147011072
mix.line =
mix.real = 888

В программе определено объединение mix, сопоставляющее один и тот же участок памяти длиной 4 байта с данными разных типов long, char[4], float. Элементы объединения mix. integer, mix. line, mix.real используются в качестве правых операндов операций извлечения из потока >> и включения в поток <<. В зависимости от типа операнда одна и та же последовательность символов, набираемая на клавиатуре (в примере это 888), воспринимается и заносится в память либо как char [4], либо как long, либо как float. При выводе в поток cout одно и то же внутреннее значение участка памяти, отведенного для объединения mix, воспринимается и отображается на экране по-разному в зависимости от типа правого операнда. В библиотеке ввода-вывода Си++ для обеспечения указанных возможностей используется тот же самый механизм перегрузки. Объекты cout, cin с операциями <<, >> "знают", как выполнять ввод-вывод значений разных типов.

Еще раз обратим внимание на результаты выполнения программы. "Правильно" выводятся значения именно тех типов, которые введены. "Неправильные" значения других типов не всегда понятны. Например, после ввода nix. real строка mix. line почему-то оказалась пустой. По-видимому, в первом байте массива char line [4] находится код нуля. Объяснение "неправильных" результатов вывода требует рассмотрения внутренних представлений, которые различны на разных ЭВМ, для разных компиляторов и даже для разных исполнений программы.

Некоторые особенности операций вставки и извлечения. Обратите внимание, что операции >> и << обеспечивают связи с потоками только в том случае, если они употребляются справа от имен потоковых объектов. В противном случае они как обычно обозначают операции сдвига. В соответствии с синтаксисом языка (см. табл. 2.4 "Приоритеты операций") операции сдвига <<, >> имеют невысокий приоритет. Им "предшествуют", например, все арифметические операции, преобразования типов, скобки и др. Использование операций <<, >> для обозначения передач данных в потоки и из потоков не изменяет их приоритета. Поэтому допустима, например, такая запись:

cout << 2 + 3 + 4;

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

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

cout << (а + b < с);

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

cout << х < 0? -х: х;

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

cout << (х < 0? -х: х);

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

cout << (2 << 1);

выведет в поток (и на экран) значение 4.

Выражения, в которые входят операции >> и <<, должны иметь значения. В соответствии с определением, находящимся в файле iostream.h, значением выражения

cout << выражение

является ссылка на объект cout, т.е. операция включения << возвращает ссылку на тот потоковый объект, который указан слева от нее в выражении. Следовательно, к результату выполнения операции включения можно вновь применить операцию <<, как и к объекту cout. Таким образом рационально применять "цепочки" операций вывода в поток. Например, так:

cout << "\nх * 2 = " << х * 2;

С помощью скобок можно следующим образом пояснить порядок вычисления этого выражения:

(cout << "\nх * 2 = ") << х * 2;

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

х * 2 = 66

Для k равного 1 после выполнения оператора

cout << "\n k * 2 = " << (k << 1) <<
" k << 2 = " << (k << 2);

результат на экране дисплея будет таким:

k*2=2          k << 2 = 4

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

int k = 1;
cout << "\nk++ = " << k++ <<
" (k += 3) = " << (k += 3)   << "   k++ = " << k++;

на экране получим:

k++ = 5     (k +>> 3) = 5     k++ = 1

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

"Цепочки" операций обмена можно формировать и при вводе данных (при их извлечении, т.е. чтении) из потока. Например, для int i, j, k, l; следующий оператор:

cin >> i >> j >> k >> 1;

обеспечивает ввод последовательности целых значений Переменных i, j, k, l. Элементы этой последовательности во входном потоке должны разделяться обобщенными пробельными символами (пробелами, знаками табуляции, символами перевода строк). Исходные данные можно ввести либо размещая их на экране в одну строку (извлечение из потока cin происходит только после сигнала от клавиши Enter):

1     2     3     4 < Enter >

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

1 < Enter >
2 < Enter >
3 < Enter >
4 < Enter >

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

1     2     3     4     5     6     7     8     9 < Enter >

При вводе и выводе значений элементов массивов необходимо явно использовать их обозначения в виде индексированных переменных. Например, попытка выполнить операторы:

float real[3] = { 10.0, 20.0, 30.0 };
cout << real;

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

cout << reаl[0] << " " << reа1[1] << " " << real [2];

При попытке ввести значения элементов массива с помощью оператора

cin >> real;

получим сообщение об ошибке на этапе компиляции. Для операторов

double e[5];
for (int i = 0; i < 5; i++) cin >> e[i];

на клавиатуре необходимо набрать, например, такую последовательность значений:

0.01      0.02      0.03      0.04      0,05 < Enter >

При вводе-выводе целых чисел существуют ограничения на длины внешнего и внутреннего представлений. Например, если при выполнении операторов

int i; cin >> i; cout << "\ni = " << i;

набрать на клавиатуре число 123456789, то результатом, выведенным на экран дисплея, будет (в конкретном случае)

i = -13035

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

cin >> g; cout << "\ng = " << g;

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

cout << 123456789;

все будет в порядке - именно такое значение будет выведено на экран. Оператор с недопустимой константой, например, такой:

cout << 98765432100;

приведет к выводу неверного числового значения. В соответствии с ограничениями реализации компилятора ВС++ справа от знака включения << можно записывать целые константы от 0 до 4294967295 (см. табл. 2.1).

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

int N; cin >> N; cout << "\nN = " << N;

можно ввести восьмеричное значение 077777 и получить ответ в десятичном виде:

N = 32767

Введя шестнадцатеричное значение -Ох7FFF, получим

N = -32767

и т.д.

Значения указателей (т.е. адреса) выводятся в стандартный поток в шестнадцатеричном виде, как мы это видели в гл. 5. Вывод числового значения, например типа int, в шестнадцатеричном или восьмеричном виде по умолчанию не выполняется. Для смены десятичного основания необходимо специальное "воздействие" на поток. Средства такого управления потоком будут рассмотрены позже, хотя в программе Р5-ОЗ.СРР мы уже использовали флаг hex, переключающий поток cout на вывод числовых значений в шестнадцатеричной форме.

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

float pi; cin >> pi; cout << "pi = " << pi;

при вводе числового значения в виде 3.141593, или 3.l4l593e0, или +3.141593, или 0. 3l4l593e+l всегда приведут к печати:

pi = 3.141593

Если при вводе вещественного значения набрать на клавиатуре:

3.1415926535897932385 < Enter >

то в выходной поток cout опять будет выведено

pi = 3.141593

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

float at; cin >> at;
cout << "\nat = " << at;

При вводе с клавиатуры значения

0.00000000000000000000001 < Enter >

на экран будет выведено:

at = le-23

Тот же результат будет выведен, если на клавиатуре набрать 1.Оe-23.

Наличие буфера в стандартном входном потоке создает некоторые особенности. В процессе набора данных на клавиатуре они отображаются на экране, но не извлекаются из потока. Это дает возможность исправлять допущенные ошибки во вводимых данных до того, как значения будут выбраны из входного потока. Извлечение данных из потока, т.е. собственно выполнение операции cin >> происходит только после нажатия клавиши Enter. При этом вся набранная строка переносится в буфер ввода, и именно из буфера ввода начинается "чтение". При извлечении числовых данных игнорируются начальные пробельные символы. Чтение начинается с первого не пробельного символа и заканчивается при появлении не числового символа. При вводе целых читаются символы ' + ', ' - ', десятичные цифры и, если число вводится в шестнадцатеричном виде (признак Ох), то буквы А, а, B, b, C, с, D, D, Е, E, F, f. Для вещественных чисел дополнительно может появиться символ Е или e, как обозначение экспоненциальной части числа, и точка, отделяющая целую часть от дробной. Выборка из входного потока прекращается, как только очередной символ окажется недопустимым. Например, если для операторов

int К; float Е;
cin >> К >> Е;
cout << "К = " << К << " Е = " << Е;

набрать на клавиатуре

1234.567     89 < Enter >

то получим

К = 1234      Е = 0.567

Здесь точка явилась недопустимым символом для K, но корректно воспринята при вводе значения Е. Символы 89 из входного потока проигнорированы, так как извлечение закончилось при достижении пробела, а больше операция >> не применяется.

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

double D;
cin >> D;
cout << "D = " << D;

введя с клавиатуры

112233445566778899 < Enter >

получим округленное значение:

D = 1.122334e+17

Ввод-вывод массивов и символьных массивов-строк - это различающиеся процедуры. Как известно, символьная строка всегда представляется как массив типа char, последним значащим элементом в котором является литера ' \о '. Именно до этой литеры оператор вывода переносит в выходной поток символы из памяти:

char H[] = "Qui pro quo - путаница"; cout << "\n" << Н;

На экране дисплея:

Qui pro quo - путаница

Если в той же программе добавить операторы

сhаr "рH = H; cout << "\n" << рН;

то результат не изменится - будет выведена строка, с которой связан указатель рН. Операция вывода <<, "настроенная" на операнд типа char *, всегда выводит строку, а не значение указателя, связанного с этой строкой. Чтобы вывести не значение строки, а значение указателя, необходимо явное приведение типа. Например, оператор

cout << "\пуказатель = " << (void *)pH;

выведет не значение строки, начало которой есть значение указателя, а собственно адрес. Для указателя, не связанного с символьной строкой (т.е. не имеющего типа char *), и вывод указателя, и вывод результата его приведения к типу (void *) будут одинаковы. Например, выполнение операторов:

int *pi,i = 6; pi = &i;
cout << "\npi = " << pi;
cout << "\n(void *)pi = " << (void *)pi;

приведет к печати одинаковых значений:

pi = Ох106e
(void*) pi = Ох106e

Интересно, что в некоторых случаях операторы

char cc[5] - { 'а', 'b', 'с', 'd', 'e' };
cout << "\nс = " << cc;

приведут к выводу

cc = abcde

но надеяться на это нельзя.

При вводе строки на клавиатуре набираются любые символы до тех пор, пока не будет нажата клавиша Enter. Например, для операторов

char line[255], stroka[80];
cin >> line >> stroka;

на клавиатуре может набираться любая последовательность символов, пока не появится код от клавиши Enter. Система ввода-вывода переносит эту последовательность в буфер входного потока, а из буфера при выполнении каждой операции >> извлечение происходит до ближайшего пробела. Вместо пробельного символа заносится код ' \0 ', тем самым, завершая строку. Если при выполнении этого оператора ввода набрать на клавиатуре:

ERGO (следовательно) (лат.)

получим line=="ERGO", И stroka==" (следовательно) ". СИМВОЛЫ " (лат.) " не будут восприняты.

Так как операции <<, >> "настроены" на заранее определенные типы данных, то иногда возникают несоответствия, которых могло бы не быть при использовании библиотечных функций ввода scanf() и вывода printf() из стандартной библиотеки языка Си. Например, рассмотрим попытку вывести на экран (в поток cout) значение символа, получаемого от клавиатуры с помощью библиотечной функции getch (), описанной в файле conio.h:

cout << "От клавиатуры поступил символ: " << getch();

Если на клавиатуре нажать клавишу 'в', то на экране получим:

От клавиатуры поступил символ: 66

Дело в том, что функция getch () ввода символа от клавиатуры без отображения на экране возвращает значение типа int, т.е. имеет прототип:

int getch(void);

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

cout << (char)getch();

11.3. Форматирование данных при обменах с потоками

Форматирование пересылаемых данных. Непосредственное применение операций вывода << (включение в поток) и ввода >> (извлечение из потока) к стандартным потокам cout, cin, cerr, clog для данных базовых типов приводит к использованию "умалчиваемых" форматов внешнего представления пересылаемых значений. Например, при выводе чисел каждое из них занимает ровно столько позиций, сколько необходимо для его представления. Это не всегда удобно и правильно. Например, выполнение операторов

int i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5;
cout << "\n" << i1 << i2 << i3 << i4 << i5;

приведет к такому результату:

12345

Для улучшения читаемости проще всего явно ввести разделительные пробелы. Выполнив, оператор

cout << "\n"<< i1<<' '<< i2<<' '<< i3<<' '<< i4<<' '<< i5;

получим более наглядный результат:

1   2   3   4   5

Следующий шаг - добавление пояснительного текста и(или) символов табуляции. Эти приемы мы уже неоднократно применяли в программах, но никак не изменяли 'формат самих выводимых значений. Ширина (количество позиций) внешнего представления каждого числа выбирается автоматически, исходя из необходимого количества позиций. Единообразие не всегда устраивает пользователя программы. Например, периодическую дробь l.0 / 3.0 можно представить весьма различными способами:

0.3     0.3333     3.3е-1     О.3333333еО

Однако стандартное представление при выводе с помощью оператора

cout << "\n1.0 / 3.0 = " << 1.0 / 3.0;

будет всегда одинаковым:

1.0 / 3.0 = 0.333333

Такое поведение выходного потока при использовании операции включения со значением типа double предусматривается по умолчанию. Форматы представления выводимой информации и правила восприятия данных, вводимых из потока, могут быть изменены программистом с помощью флагов форматирования. Эти флаги унаследованы всеми потоками библиотеки из базового класса ±оs. Флаги реализованы в виде отдельных фиксированных битов чисел типа long, поэтому несколько флагов с помощью логических битовых выражений можно объединять, тем самым по-разному комбинируя свойства потока.

Перечислим флаги форматирования, объясняя их действия для тех значений, которые указаны справа от знаков присваивания:

skipws = 0х0001

при таком значении флага операция извлечения из потока >> будет игнорировать (пропускать) обобщенные пробельные символы;

left = 0х0002

вывод значения с левым выравниванием (прижать к левому краю поля);

right = 0х0004

вывод значения с правым выравниванием (это значение устанавливается по умолчанию);

internal = 0х0008

принятый в качестве заполнителя символ (по умолчанию пробел) помещается между числовым значением и знаком числа либо признаком основания системы счисления (см. ниже компонент ios::x_f ill);

dec = 0х0010

десятичная система счисления;

oct = 0х0020

восьмеричная система счисления;

hex = 0х0040

шестнадцатеричная система счисления;

showbase = 0х0080

напечатать при выводе признак системы счисления (Ох для шестнадцатеричных чисел, 0 - для восьмеричных чисел);

showpoint = 0х0100

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

uppercase = 0х0200

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

showpos = 0х0400

печатать знак числа (символ '+') при выводе положительных чисел;

scientific = 0х0800

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

fixed = 0х1000

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

unitbuf = 0х2000

очищать все потоки (выгрузить содержимое буферов) после каждого вывода (после включения в поток);

stdio = 0х4000

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

Все флаги форматирования в виде отдельных фиксированных битов входят в компонент класса ios:

long x_flags; // Переменная представления флагов
// форматирования

Именно эта переменная, относящаяся к конкретному потоку, анализируется при обменах и влияет на преобразование информации. В библиотеке классов ввода-вывода существуют принадлежащие классу ios функции flags () и self () для проверки значений перечисленных флагов, для установки флагов и для их сбрасывания в исходные (умалчиваемые) состояния. Флаги могут обрабатываться как по отдельности, так и группами, для чего используют дизъюнктивные выражения, в которых флаги связаны побитовой операцией ' | ' (ИЛИ).

Кроме флагов для управления форматом используются следующие компонентные переменные класса ios:

int x_width

задает минимальную ширину поля вывода;

int x_precision

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

int x_fill

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

[Оглавление]
[Вверх]
 

Hosted by uCoz