Дружественные функции и перегрузка операций.(5 час.)

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

Классы.(3 час.)

Объявления переменных.

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

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

struct date { int month, day, year; };

// дата: месяц, день, год }

date today;

void set_date(date*, int, int, int);

void next_date(date*);

void print_date(date*);

// ...

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

struct date {

int month, day, year;

void set(int, int, int);

void get(int*, int*, int*);

void next();

void print();

};

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

date today; // сегодня

date my_burthday; // мой день рождения

void f()

{

my_burthday.set(30,12,1950);

today.set(18,1,1985);

my_burthday.print();

today.next();

}

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

void date::next()

{

if ( ++day > 28 ) {

// делает сложную часть работы

}

}

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

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

class date {

int month, day, year;

public:

void set(int, int, int);

void get(int*, int*, int*);

void next();

void print();

};

Метка public делит тело класса на две части. Имена в первой, закрытой части (private), могут использоваться только функциями членами. Вторая, открытая часть, составляет интерфейс к объекту класса. Обе эти части составляют реализацию объекта. Struct - это просто class, у которого все члены общие, поэтому функции члены определяются и используются точно так же, как в предыдущем случае. Описание date в предыдущем примере дает множество функций для работы с date, но не указывает, что эти функции должны быть единственными для доступа к объектам типа date. Это ограничение можно наложить используя вместо struct class:

class date {

int month, day, year;

public:

void set(int, int, int);

void get(int*, int*, int*);

void next();

void print();

};

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

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

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

class date {

date(int, int, int);

};

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

 

date today = date(23,6,1983);

date xmas(25,12,0); // сокращенная форма (xmas - рождество)

date my_burthday; // недопустимо, опущена инициализация

Часто бывает хорошо обеспечить несколько способов инициализации объекта класса. Это можно сделать, задав несколько конструкторов. Например:

class date {

int month, day, year;

public:

// ...

date(int, int, int); // день месяц год

date(char*); // дата в строковом представлении

date(int); // день, месяц и год сегодняшние

date(); // дата по умолчанию: сегодня

};

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

date today(4);

date july4("Июль 4, 1983");

date guy("5 Ноя");

date now; // инициализируется по умолчанию

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

class date {

int month, day, year;

public:

// ...

date(int d =0, int m =0, int y =0);

date(char*); // дата в строковом представлении

};

date::date(int d, int m, int y)

{

day = d ? d : today.day;

month = m ? m : today.month;

year = y ? y : today.year;

// проверка, что дата допустимая

// ...

}

Когда используется значение параметра, указывающее "брать по умолчанию", выбранное значение должно лежать вне множества возможных значений параметра. Для дня day и месяца month ясно, что это так, но для года year выбор нуля неочевиден. К счастью, в европейском календаре нет нулевого года . Сразу после 1 г. до н.э. (year=-1) идет 1 г. н.э. (year=1).

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

date d = today; // инициализация посредством присваивания

По существу, имеется конструктор по умолчанию, определенный как побитовая копия объекта того же класса. Если для класса X такой конструктор по умолчанию нежелателен, его можно переопределить конструктором с именем X(X&).

Деструкторы и очистка. Определяемый пользователем тип чаще имеет, чем не имеет, конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие, деструктор, чтобы обеспечить соответствующую очистку объектов этого типа. Имя деструктора для класса X есть ~X() ("дополнение конструктора"). В частности, многие типы используют некоторый объем памяти из свободной памяти, который выделяется конструктором и освобождается деструктором. Заметим, что в Си++ для этого используются операторы new и delete. Пример конструктора и деструктора объекта date:

class date { int *day, *month, *year

public:

date(int d, int m, int y)

{

day=new int;

month=new int;

year=new int;

*day= d ? d : 1;

*month = m ? m : 1;

*year = y ? y : 1;

}

...

~date()

{

delete day;

delete month;

delete year;

}

};

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

date date2 = date1;

Однако имеются случаи, в которых создание объекта без вызова конструктора осуществляется неявно:

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

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

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

· - dat2 в приведенном определении;

· - создаваемого в стеке формального параметра;

· - временного объекта, сохраняющего значение, возвращаемое функцией.

Вместо этого в них копируется содержимое объекта-источника:

· - dat1 в приведенном примере;

· - фактического параметра;

· - объекта - результата в операторе return.

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

class string

{

char *Str;

int size;

public:

string(string&); // Конструктор копирования

};

string::string(string& right) // Создает копии динамических

{ // переменных и ресурсов

s = new char[right->size];

strcpy(Str,right->Str);

}

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

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

class x {

int m;

public:

int readm() { return m; }

};

x aa;

x bb;

void f()

{

int a = aa.readm();

int b = bb.readm();

// ...

}

В первом вызове члена readm() m относится к aa.m, а во втором - к bb.m.

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

x* this;

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

class x {

int m;

public:

int readm() { return this->m; }

};

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

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

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

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

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

#include <stdio.h>

class dat

{

int day,month,year;

static dat *fst; // Указатель на первый элемент

dat *next; // Указатель на следующий элемент

public:

void show(); // Просмотр всех объектов

dat(); // Конструктор

~dat(); // Деструктор

};

dat *dat::fst=NULL; // Определение статического

// элемента

void dat::show()

{

dat *p;

for (p=fst; p !=NULL; p=p->next)

{ /* вывод информации об объекте */ }

}

//------ Конструктор - включение в начало списка -------- dat::dat()

{ /* ... */ next = fst; fst = this; }

//------ Деструктор - поиск и исключение из списка -------

dat::~dat()

{

dat *&p = fst; // Ссылка на указатель на

// текущий элемент списка

for (; p !=NULL; p = p->next)

if (p = this) // Найден - исключить и

{ p = p->next; return;} // и выйти

}

В данном примере используется ссылки на указатель текущего элемента списка (неявный указатель на указатель текущего элемента списка).

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

class list

{ ...

static void show(); // Стaтическая функция просмотра

} // списка объектов

static void list::show()

{

list *p;

for (p=fst; p !=NULL; p=p->next)

{ ...вывод информации об объекте... }

}

void main()

{ ...

list::show(); // Вызов функции по полному имени

}

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

class task {

// ...

task* next;

static task* task_chain;

void shedule(int);

void wait(event);

// ...

};

Описание члена task_chain (цепочка задач) как static обеспечивает, что он будет всего лишь один, а не по одной копии на каждый объект task. Он все равно остается в области видимости класса task, и "извне" доступ к нему можно получить, только если он был описан как public. В этом случае его имя должно уточняться именем его класса:

task::task_chain

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

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

class list

{ ...

static void show(); // Стaтическая функция просмотра списка

}

static void list::show()

{

list *p;

for (p=fst; p !=NULL; p=p->next)

{ ...вывод информации об объекте... }

}

void main()

{ ...

list::show(); // Вызов функции по полному имени

}

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

struct cl

{

char* val;

void print(int x) { cout << val << x << "\n"; };

cl(char* v) { val = v; }

};

// ``фальшивый'' тип для функций членов:

typedef void (*PROC)(void*, int);

main()

{

cl z1("z1 ");

cl z2("z2 ");

PROC pf1 = PROC(&z1.print);

PROC pf2 = PROC(&z2.print);

z1.print(1);

(*pf1)(&z1,2);

z2.print(3);

(*pf2)(&z2,4);

}

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

C++ поддерживает понятие указатель на член: cl::* означает "указатель на член класса cl". Например:

typedef void (cl::*PROC)(int);

PROC pf1 = &cl::print; // приведение к типу ненужно

PROC pf2 = &cl::print;

Для вызовов через указатель на функцию - член используются операции . и ->. Например:

(z1.*pf1)(2);

((&z2)->*pf2)(4);)

Структуры и объединения. По определению struct - это просто класс, все члены которого общие, то есть

struct s { ...

есть просто сокращенная запись

class s { public: ...

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

Именованное объединение определяется как struct, в которой все члены имеют один и тот же адрес. Если известно, что в каждый момент времени нужно только одно значение из структуры, то объединение может сэкономить пространство. Например, можно определить объединение для хранения лексических символов Cи - компилятора:

union tok_val {

char* p; // строка

char v[8]; // идентификатор (максимум 8 char)

long i; // целые значения

double d; // значения с плавающей точкой

};

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

void strange(int i)

{

tok_val x;

if (i)

x.p = "2";

else

x.d = 2;

sqrt(x.d); // ошибка если i != 0

}

Кроме того, объединение, определенное так, как это, нельзя инициализировать. Например:

tok_val curr_val = 12; // ошибка: int присваивается tok_val'у

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

union tok_val {

char* p; // строка

char v[8]; // идентификатор (максимум 8 char)

long i; // целые значения

double d; // значения с плавающей точкой

tok_val(char*); // должна выбрать между p и v

tok_val(int ii) { i = ii; }

tok_val() { d = dd; }

};

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

void f()

{

tok_val a = 10; // a.i = 10

tok_val b = 10.0; // b.d = 10.0

}

Когда это невозможно (для таких типов, как char* и char[8], int и char, и т.п.), нужный член может быть найден только посредством анализа инициализатора в ходе выполнения или с помощью задания дополнительного параметра. Например:

tok_val::tok_val(char* pp)

{

if (strlen(pp) <= 8)

strncpy(v,pp,8); // короткая строка

else

p = pp; // длинная строка

}

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

class tok_val {

char tag;

union {

char* p;

char v[8];

long i;

double d;

};

int check(char t, char* s)

{ if (tag!=t) { error(s); return 0; } return 1; }

public:

tok_val(char* pp);

tok_val(long ii) { i=ii; tag='I'; }

tok_val(double dd) { d=dd; tag='D'; }

long& ival() { check('I',"ival"); return i; }

double& fval() { check('D',"fval"); return d; }

char*& sval() { check('S',"sval"); return p; }

char* id() { check('N',"id"); return v; }

};

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

tok_val::tok_val(char* pp)

{

if (strlen(pp) <= 8) { // короткая строка

tag = 'N'

strncpy(v,pp,8); // скопировать 8 символов

}

else { // длинная строка

tag = 'S'

p = pp; // просто сохранить указатель

}

}

Тип tok_val можно использовать так:

void f()

{

tok_val t1("short"); // короткая, присвоить v

tok_val t2("long string"); // длинная строка, присвоить p

char s[8];

strncpy(s,t1.id(),8); // ok

strncpy(s,t2.id(),8); // проверка check() не пройдет

}

Константные члены-функции и константные объекты.Иногда требуются исключения из правил доступа, когда некоторой функции или классу требуется разрешить доступ к личной части объекта класса. Это согласуется с тем принципом, что сам класс определяет права доступа к своим объектам "со стороны". К средствам контроля доступа относятся объявления функций-членов константными (const). В этом случае они не имеют права изменять значение текущего объекта, с которым вызываются. Заголовок такой функции при этом имеет вид

void dat::put() const

{ ... }

Аналогично можно определить константные объекты:

const class a{...} value;

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

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

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

class A{

int x; // Личная часть класса

...

friend class B; // Функции класса B дружественны A

// (имеют доступ к приватной части A)

friend void C::fun(A&);// Элемент-функция fun класса C имеет

// доступ к приватной части A

friend void xxx(A&,int);// Функция xxx дружественна классу A

friend void C::operator+(А&);

// Переопределяемая в классе C операция

}; // <объект C>+<объект A> дружественна

// классу A

class B // Необходим доступ к личной части A

{

public: int fun1(A&);

void fun2(A&);

};

class C

{

public: void fun(A&);

void operator+(A&);

};

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

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

Можно описывать функции, определяющие значения следующих операций:

+ - * / % ^ & | ~ !

= < > += -= *= /= %= ^= &=

|= << >> >>= <<= == != <= >= &&

|| ++ -- [] () new delete

Последние четыре - это индексирование, вызов функции, выделение свободной памяти и освобождение свободной памяти. Изменить приоритеты перечисленных операций невозможно, как невозможно изменить и синтаксис выражений. Нельзя, например, определить унарную операцию % или бинарную !. Невозможно определить новые лексические символы операций, но в тех случаях, когда множество операций недостаточно, вы можете использовать запись вызова функции. Используйте например, не **, а pow(). Эти ограничения могут показаться драконовскими, но более гибкие правила могут очень легко привести к неоднозначностям. Например, на первый взгляд определение операции **, означающей возведение в степень, может показаться очевидной и простой задачей, но подумайте еще раз. Должна ли ** связываться влево (как в Фортране) или вправо (как в Алголе)? Выражение a**p должно интерпретироваться как a*(*p) или как (a)**(p)? Имя функции операции есть ключевое слово operator (то есть, операция), за которым следует сама операция, например, operator<<. Функция операция описывается и может вызываться так же, как любая другая функция. Использование операции - это лишь сокращенная запись явного вызова функции операции. Например:

void f(complex a, complex b)

{

complex c = a + b; // сокращенная запись

complex d = operator+(a,b); // явный вызов

}

Для переопределения операции используется особая форма функции-элемента с заголовком такого вида:

operator операция( список_параметров-операндов)

Имя функции состоит из ключевого слова operator и символа данной операции в синтаксисе языка Си. Список формальных параметров функции соответствует списку операндов, определяя их типы и способы передачи. Результат функции (тип, способ передачи) является одновременно результатом переопределенной операции.

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

· если функция задается как обычная функция-элемент класса, то первым операндом операции является объект класса, указатель на который передается неявным параметром this;

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

Естественно, что полное имя такой функции не содержит имени класса.

Бинарная операция может быть определена или как функция член, получающая один параметр, или как функция друг, получающая два параметра. Таким образом, для любой бинарной операции @ aa@bb может интерпретироваться или как aa.operator@(bb), или как operator@(aa,bb). Если определены обе, то aa@bb является ошибкой. Унарная операция, префиксная или постфиксная, может быть определена или как функция член, не получающая параметров, или как функция друг, получающая один параметр. Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@(), или как operator@(aa). Если определена и то, и другое, то и aa@ и @aa являются ошибками. Рассмотрим следующие примеры:

class X {

// друзья

friend X operator-(X); // унарный минус

friend X operator-(X,X); // бинарный минус

friend X operator-(); // ошибка: нет операндов

friend X operator-(X,X,X); // ошибка: тернарная

// члены (с неявным первым параметром: this)

X* operator&(); // унарное & (взятие адреса)

X operator&(X); // бинарное & (операция И)

X operator&(X,X); // ошибка: тернарное

};

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

В качестве примера рассмотрим доопределение стандартных операций над датами:

static int days[]={ 0,31,28,31,30,31,30,31,31,30,31,30,31};

class dat {

int day,month,year;

public:

void next(); // Элемент-функция вычисления следующего дня

dat operator++(); // Операция ++

dat operator+(int);// Операция "дата + целое" с передачей

// первого операнда через this

friend dat operator+(int,dat);// Операция с явной передачей всех

//аргументов по значению

dat(int=0,int=0,int=0);

dat(char *); //

~dat(); //

}; //

//------ Функция вычисления следующего дня -----------------

// Используется ссылка на текущий объект this,

// который изменяется в процессе операции

void dat::next()

{

day++;

if (day > days[month])

{

if ((month==2) && (day==29) && (year%4==0)) return;

day=1; month++;

if (month==13)

{

month=1; year++;

}

}

}

//------ Операция инкремента даты -------------------------

// 1. Первый операнд по указателю this

// 2. Возвращает копию входного объекта (операнда)

// до увеличения

// 3. Соответствует операции dat++ (увеличение после

// использования)

// 4. Замечание: для унарных операций типа -- или ++

// использование их до или после операнда не имеет

// значения (вызывается одна и та же функция).

dat dat::operator++()

{ // Создается временный объект

dat x = *this; // В него копируется текущий объект

dat::next(); // Увеличивается значение текущего объекта

return(x); // Возвращается временный объект по

} // значению

//------ Операция "дата + целое" --------------------------

// 1. Первый операнд по указателю this

// 2. Входной объект не меняется, результат возвращается

// в виде значения автоматического объекта x

dat dat::operator+(int n)

{

dat x;

x = *this; // Копирование текущего объекта в x

while (n-- !=0) x.next();// Вызов функции next для объекта x

return(x); // Возврат объекта x по значению

}

//------ Операция "целое + дата" -------------------------

// 1. Дружественная функция с полным списком операндов

// 2. Второй операнд класса dat - передается по значению,

// поэтому может модифицироваться без изменения исходного

// объекта

dat operator+(int n, dat p)

{

while (n-- !=0) p.next(); // Вызов функции next для p

return(p); // Возврат копии объекта p

}

void main()

{

int i;

dat a, b(17,12,1990), c(12,7), d(3), e;

dat *p = new dat[10];

e = a++;

d = b+15;

for (i=0; i<10; i++) p[i] = p[i] + i;

delete[10] p;

}

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

//------ Операция "дата + целое" --------------------------

// 1. Функция с неявным первым операндом по указателю this

// 2. Меняется значение текущего объекта

// 3. Результат - ссылка на текущий объект

dat& dat::operator+(int n)

{

while (n-- !=0) next(); // Вызов next с текущим объектом

return(*this); // Возврат ссылки на объект this

}

//------ Операция "дата + целое" -------------------------

// 1. Дружественная функция с полным списком аргументов

// 2. Первый операнд класса dat - ссылка, меняется при

// выполнении операции

// 3. Результат - ссылка на операнд

dat& operator+(dat& p,int n)

{

while (n-- !=0) p.next(); // Вызов next для объекта p,

// заданного ссылкой

return(p); // Возврат ссылки на p

}

//----- Операция "целое + дата" --------------------------

// 1. Дружественная функция с полным списком аргументов

// 2. Второй операнд класса dat - ссылка, меняется при

// выполнении операции

// 3. Результат - ссылка на операнд

//--------------------------------------------------------

dat& operator+(int n, dat& p)

{

while (n-- !=0) p.next(); // Вызов next для объекта p,

// заданного ссылкой

return(p); // Возврат ссылки на p

}

void main()

{

dat a,b; // "Арифметические" эквиваленты

a + 2 + 3; // a = a + 2; a = a + 3;

5 + b + 4; // b = 5 + b; b = b + 4;

}

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

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

//------ Операция "дата + целое" --------------------------

// 1. Функция с неявным первым операндом по указателю this

// 2. Изменяется автоматический объект - копия операнда

// 3. Результат - значение автоматического объекта

dat dat::operator+(int n)

{

dat tmp = *this; // Объект - копия операнда

while (n-- !=0) tmp.next();// Вызов next с объектом tmp

return(tmp); // Возврат значения объекта tmp

}

//------ Операция "дата + целое" -------------------------

// 1. Дружественная функция с полным списком аргументов

// 2. Первый параметр класса dat передается по значению,

// является копией первого операнда и меняется при

// выполнении операции

// 3. Результат - значение формального параметра

dat operator+(dat p,int n)

{

while (n-- !=0) p.next(); // Вызов next для объекта p,

// копии операнда

return(p); // Возврат значения

} // формального параметра

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

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

class string

{

char *Str;

int size;

public:

string &operator =(string&);

};

string &string::operator=(string& right)

{

if (Str !=NULL) delete Str;// Освободить динамическую память

size = Str.right.size; // Резервировать память

Str = new char[size];

strcpy(Str,right->Str); // Копировать строки

}

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

//------Переопределение операций [] и ()

#include <string.h>

class string // Строка переменной длины

{

char *Str; // Динамический массив символов

int size; // Длина строки

public:

string operator()(int,int); // Операция выделения подстроки

char operator[](int); // Операция выделения символа

int operator[](char*); // Операция поиска подстроки

};

//------ Операция выделения подстроки -------------------

string string::operator()(int n1, int n2) {

string tmp = *this;

delete tmp.Str;

tmp.Str = new char[n2-n1+1];

strncpy(tmp.Str, Str+n1, n2-n1); }

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

Рассмотрим пример применения разных операций - функций для постфиксной и префиксной операций ++ и --.

class pair // класс «пара чисел»

{ int N; // целое

double x; // вещественное

friend pair& operator ++ (pair&); //дружественная для префикса

friend pair& operator ++(pair&,int);//дружественная для постфикса

public:

pair (int n, double xn) //конструктор

{ N = n; x = xn; }

void display () //вывод значения

{ printf (”N = % d x = % f\n”, N, x); }

pair & operator –- () //член для префикса

{ N /= 10; x /= 10; return *this; }

pair & operator –- ( int k) //член для постфикса

{ N /= 2; x /= 2; return *this; }

};

pair & operator ++ ( pair & P) // дружественная для префикса

{ P.N *= 10; P.x *= 10; return P; }

pair & operator ++ (pair & P, int k)// дружественная для постфикса

{ P.N = P.N * 2 + k; P.x = P.x * 2 + k; return P; }

void main ()

{ pair Z (10, 20.0); //вызов конструктора

Z.display(); //N = 10 x = 20

++Z; Z.display(); //N = 100 x = 200

--Z; Z.display(); //N = 10 x = 20

Z++; Z.display(); //N = 20 x = 40

Z--; Z.display(); //N = 10 x = 20

}

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

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

void *operator new(size_t size);

void operator delete (void *);

где void * - указатель на область памяти, выделяемую под объект, size - размер объекта в байтах, size_t - тип размерности области памяти, int или long.

Переопределение этих операций позволяет написать собственное распределение памяти для объектов класса.

Операции, не допускающие перегрузки. В С++ существует несколько операций, не допускающих перегрузки:

. прямой выбор члена объекта класса;

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

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

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

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

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

Преобразование типов, определяемых пользователем с помощью конструкторов и операций преобразования.При работе со стандартными типами данных в Си имеют место явные и неявные преобразования их типов. По аналогии для классов также могут быть определены такие операции - они ассоциируются с конструированием объектов класса. Так, если в программе встречается преобразование типа (класса) "yyy" к типу (классу) "xxx", то для его осуществления в классе "xxx" необходим конструктор вида xxx(yyy &);

Сами преобразования типов происходят в тех же самых случаях, что и обычные преобразования базовых типов данных:

· при использовании операции явного преобразования типов;

· при выполнении операции присваивания, если она не переопределена в виде "xxx=yyy" (транслятором создается временный объект класса "xxx", для которого вызывается указанный конструктор и который затем используется в правой части операции присваивания);

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

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

· при определении объекта класса "xxx" одновременно с его инициализацией объектом класса "yyy" (вместо конструктора копирования)

yyy b;

xxx a = b;

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

В качестве примера рассмотрим обратное преобразование базового типа long к типу dat - количество дней от начала летоисчисления преобразуется к дате. Здесь же рассмотрим другой класс - man, в котором одним из элементов личной части является дата. Значение этого объекта копируется при преобразовании типа man в тип dat.

static int days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};

class man;

class dat

{

int day,month,year;

public:

dat(long); // Преобразование long в dat

dat(man&); // Преобразование man в dat

dat() {}

};

class man

{

friend class dat;

dat WasBorn; // объект класса dat в объекте класса man

public:

man(dat&); // Преобразование dat в man

};

dat::dat(man& p)

{ *this = p.WasBorn; }

man::man(dat& p)

{ WasBorn = p; }

dat::dat(long p)

{

year = p / 365.25; // Число лет с учетом високосных

p-=(year-1)*365L - year/4; // Остаток дней в текущем году

year++; // Начальный год - 0001

for (month=1; p > 0; month++)

{ // Вычитание дней по месяцам

p -= days[month];

if (month == 2 && year % 4 == 0) p--;

}

month--; // Восстановление последнего

p += days[month]; // месяца

if (month == 2 && year % 4 == 0) p++;

day = p + 1;

}

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

long l=1000;

dat a = l, b; // Вызов конструктора dat(long)

man c = a; // Вызов конструктора man(dat&)

man f(man a)

{ return(a); }

void main()

{

a = 2000L; // Вызов конструктора dat(long)

(dat)3000L; // Вызов конструктора dat(long)

c = b; // Вызов конструктора man(dat&)

b = f(b); // Вызов конструктора dat(man&)

}

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

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

#include <stdio.h>

static int days[]={0,31,28,31,30,31,30,31,31,30,31,30,31};

class dat

{

int day,month,year;

public:

operator int(); // Преобразование dat в int

operator long(); // Преобразование dat в long

long operator -(dat &p); // Операция dat-dat вычисляет

dat(); // разность дат в днях

dat(char *);

};

dat::operator int()

{

int r; // Текущий результат

int i; // Счетчик месяцев

for (r=0, i=1; i<month; i++) // Число дней в прошедших

r += days[month]; // месяцах

if ((month>2) && (year%4==0)) r++; // Високосный год

r += day; // Дней в текущем месяце

return(r);

}

dat::operator long()

{

long r; // Текущий результат

r = 365 * (year-1); // Дней в предыдущих полных годах

r += year / 4; // Високосные года

r += (int)(*this); // Дней в текущем году

return(r);

}

long dat::operator-(dat& p)

{return((long)(*this) - (long)p);}

void main()

{

dat a("12-05-1990"); // Дата, заданная строкой

dat b; // Текущая дата

int c;

long d;

printf("С 12-05-1990 прошло %4ld дней\n",(long)b-(long)a);

printf("В этом году прошло %3d дней\n",(int)b);

c = b;

d = b - a; // Операция dat-dat

printf("С 12-05-1990 прошло %4ld дней\n",d);

printf("В этом году прошло %3d дней\n",c);

}

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

class X {

// ...

X(int);

int m();

friend int f(X&);

};

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

void g()

{

1.m(); // ошибка

f(1); // f(x(1));

}

Поэтому операция, изменяющее состояние объекта, должно быть членом, а не другом. Для определяемых пользователем типов операции, требующие в случае фундаментальных типов операнд lvalue (=, *=, ++ и т.д.), наиболее естественно определяются как члены. И наоборот, если нужно иметь неявное преобразование для всех операндов операции, то реализующая ее функция должна быть другом, а не членом. Это часто имеет место для функций, которые реализуют операции, не требующие при применении к фундаментальным типам lvalue в качестве операндов (+, -, || и т.д.).

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

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

5. Производные классы.(3 час.)

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

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

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

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

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

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

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

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

При наследовании классов важную роль играет статус доступа (статус внешней видимости) компонентов. Для любого класса все его компоненты лежат в области его действия. Тем самым любая принадлежащая классу функция может использовать любые компонентные данные и вызывать любые принадлежащие классу функции. Вне класса в общем случае доступны только те его компоненты, которые имеют статус 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 B { protected: int t;

public: char u;

};

class E: B { ... }; // t, и наследуются как private

class S: B { ... }; // t, и наследуются как public

Явно изменить умалчиваемый статус доступа при наследовании можно с помощью спецификаторов доступа - private, protected и public. Эти спецификаторы доступа указываются в описании производного класса непосредственно перед нужными именами базовых классов. Если класс B определен так, как показано выше, то можно ввести следующие производные классы:

class M: protected B { ... }; // t, и наследуется как protected

class P: public B { ... }; // t - protected, и - public

class D: private B { ... }; // t, и наследуется как private

struct F: private B { ... }; // t, и наследуется как private

struct G: public B { ... }; t - protected, и - public

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

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

имя_класса

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

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

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

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

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

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

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

class X1 { ... };

class X2 { ... };

class X3 { ... };

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

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

Определения базовых классов должны предшествовать их использо