Объективное программирование
if (p=this) break; // Если найден - выход
if (p !=NULL)
{ // Найден - исключение из списка
if (pred==NULL)
fst = next;
else
pred->next=next;
}
}
//-------------------------------------------------------void
list::show()
{
list *p;
for (p=fst; p !=NULL; p=p->next)
{ ...вывод информации об объекте... }
}
//------ При создании объекта он помещается в список -----------
list::list()
{
insfst();
}
//------ При уничтожении объекта он исключается из списка ------
list::~list()
{
extract();
}
Примером использования внутреннего списка объектов является
система всплывающих окон. При выполнении операций над одним из
окон часто требуется произвести некоторые действия с другими окнами, то
есть в любой момент программе должен быть известен
список созданных объектов - окон. Последовательность объектов в
списке может отражать последовательность отображения окон на экране.
Тогда при выполнении операции "всплытия" окна необходимо
изменить посложение соответствующего объекта в списке. Естественно, что
конструктор и деструктор объекта включают его в список и
исключают.
Статическими могут быть объявлены также и элементы-функции.
Их "статичность" определяется тем, что вызов их не связан с конкреетным
объектом и может быть выполнен по полному имени. Соответственно в них не
используются неявная ссылка 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(); // Вызов функции по полному имени
}
Лекция 4. Переопределение операторов.
------------------------------------
Напомним, что под классом понимается определяемый программистом тип
данных, используемый наравне со стандартными базовыми типами. С точки
зрения "равноправия" вновь вводимого типа данных желательно иметь
возможность расширения (переопределения) операций языка, в которых один
или несколько операндов могут быть объектами этого класса Это достигается
введением элемента-функции специального вида, обращение к которой
компилятор формирует при трансляции такой операции. Естественно, что такая
функция должна иметь результат (значение или неявная ссылка), если
предполагается использование этой операции внутри другого выражения.
Переопределение операций осуществляется в рамках стандартного
синтаксиса языка Си, то есть обозначение операций и количество операндов
остается прежним.
Необходимо отметить также и тот факт, что для каждой комбинации типов
операндов переопределяемой операции необходимо ввести отдельную функцию,
то есть транслятор не может производить перестановку операндов местами,
даже если базовая операция допускает это. Например, при переопределении
операции сложения объекта
класса dat с целым необходимо две функции dat+int и int+dat.
Для переопределения операции используется особая форма элемента-
функции с заголовком такого вида:
operator( )
При этом имя функции состоит из ключевого слова operator и
символа данной операции в синтаксисе языка Си.
Список формальных параметров функции является списком операндов
(количество, типы, способы передачи) операции.
Результат функции (тип, способ передачи) является результатом
переопределяемой операции. Способ передачи и тип указывают на
возможности использования результата в других выражениях.
Имеется два способа описания функции, соответствующей
переопределяемой операции:
- если функция задается как обычная элемент-функция класса,
то первым аргументом соответствующей операции является объект,
ссылка на который передается неявным параметром this;
- если первым аргументом переопределяемой операции не является объект
некоторого класса, либо функция получает на вход не
ссылку на объект, а сам объект, тогда соответствующая элементфункция
должна быть определена как дружественная с полным списком
аргументов. Естественно, что полное имя дружественной функцииоператора не
содержит при этом имени класса.
В качестве примера рассмотрим доопределение стандартных операций над
датами.
#include
#include
#include
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+(dat,int) // Операции с явной передачей
friend dat operator+(int, dat) // всех параметров по значению
dat(); // Конструкторы
dat(int,int,int); // (см. предыдущие примеры)
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. Альтернативный вариант предыдущей функции
//3. Первый операнд класса dat - передается по значению,
// поэтому может модифицироваться без изменения исходного объекта
//-------------------------------------------------------dat
operator+(dat p,int n)
{
while (n-- !=0) p.next(p); // Вызов функции next для объекта p
return(p); // Возврат копии объекта 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;
dat b(17,12,1990);
dat c(12,7);
dat d(3);
dat e;
dat *p = new dat[10];
clrscr();
e = a++;
d=b+15;
for (i=0; i2) && (year%4==0)) r++; // Високосный год
r += day; // Дней в текущем месяце
return(r);
}
//------ Преобразование dat в long ---------------------------//
Используется ссылка на текущий объект this
//------------------------------------------------------
dat::opertor long()
{
long r; // Текущий результат
r = 365 * (year-1) // Дней в предыдущих полных годах
r += year / 4; // Високосные года
r += (int)(*this); // Дней в текущем году - предыдущая
// операция (явное преобразование
return(r); // dat в int
}
//-------- Операция вычисления разницы двух дат ---------------//
Первый операнд по ссылке на текущий объект this
// Второй операнд по неявной ссылке p
//-------------------------------------------------------long
dat::operator-(dat& p)
{
return((long)(*this) - (long)p); // Преобразовать оба объекта
// к типу long и вычисл. разность
}
void main()
{
dat a("12-05-1990"); // Дата, заданная текстовой строкой
dat b; // Текущая дата
int c;
long d;
// Явное преобразование к long
printf("С 12-05-1990 прошло %4ld дней\n",(long)b-(long)a);
// Явное преобразование к int
printf("В этом году прошло %3d дней\n",(int)b);
// Неявное преобразование при присваивании
c = b;
d = b - a; // Операция dat-dat
printf("С 12-05-1990 прошло %4ld дней\n",d);
printf("В этом году прошло %3d дней\n",c);
}
5.2 Преобразование переменной к объекту класса
---------------------------------------------
Данный способ не является стандартным и требует проверки
работоспособности в используемом компиляторе. Он основан на том
факте, что при компиляции явного или неявного преобразования объекта класса
к базовому типу данных "xxx" вызывается переопределяемая операция "operator
xxx()". Соответственно, при явном или неявном преобразовании к классу "zzz"
должна вызываться переопределяемая операция "operator zzz". Логично, что
такая операция должна быть определена в классе "zzz". Но тогда имя
соответствующей элемента-функции будет "zzz::zzz", что соответствует
конструктору. Таким образом, если необходимо определить явное или неявное
преобразование от базового типа или класса "xxx" к классу "zzz",
то в классе "zzz" необходимо определить конструктор
class zzz
{
int par_zzz;
----------------- входной тип (класс)
zzz(xxx p); или
zzz(xxx& p);
L-------------------- выходной тип (класс)
};
void zzz::zzz(xxx &p)
{
par_zzz = ... p.par_xxx ...;
элемент объекта----- L-------элемент объекта
выходного класса входного класса
}
class xxx
{
friend class zzz;
int par_xxx;
};
со следующими свойствами:
- объект класса "zzz", который является выходным при преобразовании
типов доступен как в любом конструкторе через ссылку на текущий объект
this;
- элементам выходного объекта (например, par_zzz) должны быть
присвоены значения с явным или неявным использованием ссылки this
this->par_zzz = ...
(*this).par_zzz = ...
par_zzz = ...
- объект или переменная того класса или базового типа, которые
являются входными в преобразовании типов, доступны через
соответствующий формальный параметр, который может быть как значением
(копией объекта или переменной), так и неявной ссылкой.
Значение переменной или элементов входного объекта могут использоваться
как аргументы при преобразовании типов;
- для доступа из функции класса "zzz" к приватной части объекта
класса "xxx" класс "zzz" должен быть объявлен дружественным
в определении класса "xxx".
В качестве примера рассмотрим обратное преобразование базового типа
long к типу dat - количество дней от начала летоисчисления
преобразуется к дате. Здесь же рассмотрим другой класс
- man, в котором одним из элементов приватной части является дата.
static int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
class dat
{
int day,month,year;
public:
dat(long); // Преобразование long в dat
dat(man&); // Преобразование man в dat
dat(); // Конструкторы
dat(int,int,int); //
dat(char *); //
~dat(); // Деструктор
};
class man
{
friend class dat; // Класс dat дружественен
... // классу man
int d,m,y; // Элемент "дата" в объекте
... // класса man
public:
man(dat&); // Преобразование dat в man
man(); // Конструктор
~man(); // Деструктор
};
//------ Преобразование man в dat ----------------------------
//Используется ссылка на текущий объект this для выходного класса,
//формальный параметр - неявная ссылка - для входного класса
//-------------------------------------------------------void
dat::dat(man& p)
{
day = p.d;
month = p.m;
year = p.y;
}
//------ Преобразование long в dat ---------------------------
//Используется ссылка на текущий объект this для выходного класса
//формальный параметр типа long передается по значению
//-------------------------------------------------------
void dat::dat(long p)
{
year = p / 365.25; // Число лет с учетом високосных
p = 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;
}
void main()
{
dat a("12-05-1990"); // Дата, заданная текстовой строкой
dat b; // Текущая дата
int c;
long d;
// Явное преобразование к long
printf("С 12-05-1990 прошло %4ld дней\n", (long)b-(long)a);
// Явное преобразование к int
printf("В этом году прошло %3d дней\n", (int)b);
c = b; // Неявное преобразование при присваивании
d = b - a; // Операция dat-dat
printf("С 12-05-1990 прошло %4ld дней\n",d);
printf("В этом году прошло %3d дней\n",c);
}
5.3 Переопределение операций new и delete
----------------------------------------
Операции создания и уничтожения объектов в динамической памяти могут
быть переопределены следующим образом:
void *operator new(size_t size);
void operator delete (void *);
где void * - ссылка на область памяти, выделяемую под объект,
size - размер объекта в байтах.
Переопределение этих операций позволяет написать собственное
распределение памяти для объектов класса.
5.4 Переопределение операций [], (), ->
--------------------------------------
Переопределение () :
-------------------class one
{
public:
typeout operator()(type1,type2);
};
Вызов:
type1 a; // Вызов оператора совпадает с
type2 b; // синтаксисом вызова функции
one obj; // с именем данного объекта
... obj(a,b) ... эквивалентно obj.operator()(a,b)
Переопределение -> :
------------------class two
{
public: type Y;
};
class one
{
two operator->(); // Операция должна возвращать объект
или two* operator->(); // или ссылку на объект класса two,
}; // в котором определен элемент Y
Вызов:
one obj;
... obj->Y .. эквивалентно (obj.operator->()) ->Y
Переопределение [] : используется для моделирования виртуальных
-------------------- массивов элементов определенного типа.
class text_page
{
char **page; // Массив ссылок на строки
public:
int operator[](char*); // Ассоциативный поиск индекса
// по строке
char* operator[](int); // Выделение строки по индексу
};
5.5 Переопределение операции копирования объектов
------------------------------------------------
Kaк известно, определение объекта класса в виде
=
приводит к тому, что объект инициализируется путем побайтного
копирования содержимого другого объекта без вызова конструктура. K таким
же объектам относятся объекты - формальные параметры функций, которые
инициализируются копиями фактических параметров.
Eсли функция возвращает объект, то оператор return также выполняет
копирование объекта - операнда в объект назначения.
Taкое копирование не корректно в том случае, если объекты содержат
ссылки на другие объекты или переменные в динамической памяти. В этом
случае можно воспъльзоваться специальным конструктором копирования,
параметром котрого является неявная ссылка на объект - источник, а this
указывает на объект приемник. Будучи определенным, он вызывается во всех
вышеперечисленных случаях копирования объектов один в другой.
Пример корректного конструктора копирования для класса строк имеет
вид:
class string
{
char *s; // Ссылка на строку
int sz; // Длина строки
public: string(string&);// Конструктор копирования
}; // создает копию строки в динамической
// памяти для объекта - приемника
string::string(string& right)
{
s = new char[right->sz];
strcpy(s,right->s);
}
Лекция 6. Производные классы
---------------------------
6.1 Вложенные классы
-------------------
Понятие производного класса вводит в систему классов принцип
иерархии. Действительно, если определен класс объектов с достаточно
общими свойствами то объект данного класса желательно включать в качестве
одного из элементов в объекты других классов. Существует два способа
такого включения, каждый из них имеет
собственные цели и особенности.
Первый случай представляет собой обычный способ построения
инрархической структуры данных, когда объект старого класса является одним
из элементов данных "приватной" части нового класса.
Он имеет собственное имя (именован), по которому к нему можно обращаться
как к объекту. В элементах-функциях нового класса можно
использовать элементы-функции и операции для объекта старого
класса. Рассмотрим в качестве примера класс man - информация о
человеке, включающая в себя даты рождения и поступления на работу.
class man
{
char name[20]; // Другие элементы класса
char *address;
dat dat1; // Дата рождения
dat dat2; // Дата поступления на работу
public:
void newadr(); // Элемент-функция
man(char*); // Конструктор
}
//----- Функция "Изменить адрес проживания" ----------------void
man::newadr()
{
int n;
char s[80]; // Строка нового адреса
if (address != NULL)
delete address; // Освободить память
printf("Введите новый адрес:");
gets(s);
address = new char[strlen(s)+1];// Занять новую память
strcpy(address,s); // Заполнить поле адреса
}
Из данного примера видно, что именованные объекты старого класса
можно использовать в элементах-функциях нового класса как обычные
элементы, вызывать определенные для них элементы-функции старого класса и
выполнять переопределенные для них операции. Заметим, что при этом
элементы-функции нового класса не имеют доступа к приватной части
объектов базового класса, то есть "содержимое" вложенных объектов для них
закрыто.
Но здесь возникает вопрос, как инициализируются и уничтожаются
объекты старого класса при создании или уничтожении объекта нового
класса, то есть как взаимодействуют их конструкторы и деструкторы.
В случае, если конструктор объекта нового класса задан обычным
образом, то перед вызовом этого конструктора будут вызваны конструкторы
без параметров для входящих в него объектов старого класса. И наоборот,
после вызова деструктора для объекта нового класса будут вызваны
деструкторы вложенных объектов старого класса.
Однако при конструировании вложенных объектов им желательно
передавать параметры. Поэтому при описании конструктора объекта нового
класса можно в заголовке в явном виде указать тот вид конструктора
объекта старого класса, который требуется. Кроме того, его параметры могут
зависеть от параметров вызова конструктора нового класса:
class man
{
char name[20]; // Другие элементы класса
dat dat1; // Дата рождения
dat dat2; // Дата поступления на работу
public:
man(char *,char *,char *); // Конструкторы
man(char *);
}
//----- Конструктор класса man с неявным вызовом конструкторов
// для dat1 и dat2 без параметров
//-----------------------------------------------------
man::man(char *p)
{
}
//----- Конструктор класса man с явным вызовом конструкторов
// для dat1 и dat2 с параметрами
//--------------------------------------------------------- man::man(char
*p,char *p1, char *p2) : dat1(p1), dat2(p2)
{ ¦ ¦ ¦
// --- Тело конструктора --- ¦ ¦ ¦
} ¦ ¦ ¦
Вызов конструктора для ------------------ ¦ ¦
вложенного объекта dat1 ¦ ¦
В качестве параметра передается ------------- ¦
строка - второй параметр вызова ¦
конструктора для класса man Вызов конструктора для
вложенного объекта dat2
void main ------ Строка конструктора man
{ ¦
man JOHN("John","8-9-1958","15-1-1987");
} ¦ L------ Строка передается
Строка передается конструктору объекта
конструктору объекта dat2 в объекте man
dat1 в объекте man
6.2 Производные классы
---------------------
Другой случай вложенности классов основывается на понимании
класса как совокупности данных и операций над ними. При этом
принцип вложенности рассматривается как создание нового "производного"
класса, который включает в себя все или большую часть
свойств старого "базового" класса, или "наследует" их: структура
объекта старого класса включается в новый объект, а все элементы-функции
старого класса применимы к объекту нового класса, точнее к его старой
составляющей.
Старый класс при этом называется базовым классом (БК), новый
- производным классом (ПК).
Синтаксис определения производного класса имеет вид:
class : ,,...
{
определение приватной и публичной части
производного класса
}
Перечислим основные свойства базового и производного классов:
- объект базового класса определяется в производном классе как
неименованный. Это значит, что он не может быть использован в явном виде
как обычный именованный объект;
- элементы данных базового класса включаются в объект производного
класса (как правило, компилятор размещает их в начале объекта
производного класса). Oднако приватная часть базового класса закрыта
для прямого использования в производном классе;
- элементы-функции базового класса "наследуются" в производном классе,
то есть вызов функции, определенной в базовом классе, для объекта
производного класса возможен и понимается как вызов ее для входящего в
него объекта базового класса;
- в производном классе можно переопределить наследуемую функцию,
которая будет вызываться вместо наследуемой. При этом для выполнения
соответствующих действий над объектом базового класса она может
включать явный вызов переопределяемой функции.
Пример схемы определения производного класса
class a
{
public:
void f() {}
void g() {}
}
class b : a ------------------------ базовый класс
{
public: void f() -------------------- "f" переопределяется
{ ...
a::f(); -------------- явный вызов "f" для БК
} -------------- "g" наследуется из БК
void h() {} -------------- собственная функция в ПК
}
void main()
{
a A1;
b B1;
B1.f(); --------------- вызов переопределенной b::f()
B1.g(); --------------- вызов наследуемой a::f()
}
Понятие "наследования" предполагает что при вызове в производном
классе функций, наследуемых из базового, транслятор производит
преобразование ссылки this на объект производного класса в
ссылку на входящий в него объект базового класса, учитывая размещение
последнего в объекте производного класса.
Взаимоотношение конструкторов и деструкторов базового и производного
классов аналогичны выше описанным:
- если конструктор производного класса определен обычным образом, то
сначала вызывается конструктор базового класса без параметров, а затем
конструктор производного класса. Деструкторы
вызываются в обратном порядке - сначала для производного, затем
для базового;
- в заголовке конструктора производного класса может быть
явно указан вызов конструктора базового класса с параметрами. Он
может быть без имени, а может быть с именем базового класса. Если
производный класс включает в себя объекты нескольких базовых
классов, то в вызовы конструкторов базовых классов должны быть
перечислены через запятую и должны иметь имена базовых классов.
6.3 Права доступа в производных классах
--------------------------------------
Производный класс включает в себя как приватную, так и публичную
часть базового класса. При этом важно, В какую часть производного класса,
приватную или публичную, попадут соответствующие части базового класса. От
этого зависит доступность элементов
базового класса, как из элементов-функций производного класса,
так и извне - через объекты производного класса. Здесь возможны
следующие варианты:
- приватная часть базового класса A всегда включается в приватную
часть производного класса B, но при этом непосредственно
недоступна из элементовфункций класса B. Это соответствует тому
факту, что в классе B разрешается работать с базовым объектом
класса A только разрешенными в классе A средствами, то есть через элементы-
функции класса A. Исключение составляет объявление всего класса B
дружественным в классе A;
- по умолчанию, то есть при использовании заголовка вида
class B : A { }
публичная часть класса A попадает в приватную часть класса B. Это
значит, что элементы-функции класса A доступны из элементов-функций класса
B, но не могут быть вызваны извне, то есть при обращении к объектам класса
B. То есть для внешнего пользователя класса B интерфейс класса A
закрывается;
- в противном случае, при объявлении
class B : public A { }
публичная часть класса A попадает в публичную часть класса B, и
внешний пользователь при работе с объектами класса B может применить
интерфейсы как производного, так и базового классов;
- и наконец, в определении публичной части класса B можно явно
указать элементы-функции (а также данные) публичной части базового класса
A, которые попадают в публичную часть класса B, то есть выполнить
предыдущее действие селективно по отношению к отдельным элементам (при этом
указывается только имя элемента):
class B : A {
...
public:
...
public A::fun;
...
}
Перечисленные варианты изображены на схеме:
class A class B
-----------¬ ----------------¬
¦ privat ======================> privat A ¦
+----------+ ¦ (недоступен B)¦
¦ public ¦ class B:A +---------------+
¦ ======================> privat B ¦
¦ ¦ ¦ (доступен B) ¦
¦ ¦ class B : public A ¦===============¦
¦ ======================> public B ¦
¦ ¦ class B : A { ... ¦ ¦
¦ ¦ public A::newadr; ¦ производного класса
L----------- L---------------
Из рассмотренных вариантов видно, что приватная часть базового класса
недоступна в любом производном классе, что естественно следует из свойств
закрытости определения класса. Однако по
аналогии с дружественностью базовый класс может разрешить доступ
к своим элементам личной части в производных классах. Это делается при
помощи объявления защищенных (protected) элементов.
Элемент с меткой protected в базовом классе входит в приватную часть
базового класса. Кроме того, он доступен и в приватной
части производного класса. Если же базовый класс включается в
производный как public, то защищенный элемент становится защищенным и в
производном классе, то есть может использоваться в последующих производных
классах. Сказанное поясним примером и схемой:
class A
{
int a1; // Обычный приватный элемент
protected: int a2; // Защищенный приватный элемент
public:
}
class B : A // a1,a2 в приватной части B
{
void x();
}
void B::x()
{
a1 = 5; // Ошибка: a1 недоступен в B
a2 = 3; // a2 доступен в приватной части B
}
class B : public A // a2 доступен и защищен в приватной
{ // части B, неявно имеет место
// protected: int a2;
}
class A class B
-----------¬ ----------------¬
¦ privat ======================> privat A ¦
+----------+ ¦ (недоступен B)¦
¦ protected¦ class B:A +---------------+
¦ ======================> privat B ¦
¦ ===============¬ ¦ (доступен B) ¦
+----------+ class B: public A +---------------+
¦ public ¦ L======> protected B ==========>
¦ ¦ ¦===============¦
¦ ¦ ¦ public ¦
6.4 Ссылки на объекты базового и производного классов
----------------------------------------------------
Из классического Си известно, что путем присваивания ссылкам
различного типа одного и того же значения (адреса) можно работать с общей
памятью как с различными структурами данных. При этом преобразование типа
и присваивание не меняют значения ссылки, то есть адреса памяти.
Применительно к базовому и производному классу можно сказать, что,
преобразуя ссылку на объект производного класса к ссылке на объект
базового класса, мы получаем доступ к вложенному объекту базового класса.
Но при таком трактовании преобразования типа ссылки транслятору необходимо
учитывать размещение объекта базового класса в производном, что он и
делает. В результате при таком преобразовании (присваивании) значение
ссылки (адрес памяти) может оказаться не равным исходному. Ввиду того,
что такой переход от объекта производного класса к базовому часто
встречается и корректируется транслятором, это преобразование типа ссылки в
Си++ может бытьл выполнено неявно (остальные преобразования
типов ссылок должны быть явнями)
Побочный эффект такого преобразования состоит в том, что
транслятор "забывает" об объекте производного класса и вместо
переопределенных в нем функций вызывает функции базового класса.
class A
{
public: void f1();
};
class B : A
{
public: void f1(); // Переопределена в классe B
void f2(); //
};
A *pa;
B *pb;
B x;
pa = &x; // Неявное преобразование ссылки
// на объект класса B в ссылку
// на объект класса A
pa->f1(); // Вызов функции из вложенного
// объекта базового класса A::f1(),
// хотя она переопределена
Обратное преобразование от ссылки на базовый класс к ссылке
на производный может быть сделано только явно. При этом корректность
такого преобразования зависит от программы:
pb = (B*) pa; // Обратное преобразование - явное
pb ->f2(); // Корректно, если под "pa" был
// объект класса "B"
6.5 Принцип объектно-ориентированного программирования
------------------------------------------------------
Понятие производного класса является основой объектноориенированного
подхода к программированию, которое можно определить
как программирование "от класса к классу". Традиционное программирование
"от функции к функции" предполагает, что вновь разрабатываемые структуры
данных включают в себя определенные ранее, а
новые функции включают вызовы ранее определенных.
При разработке объектно-ориентированной программы программист
создает производные классы, которые автоматически наследуют
все свойства базовых, а затем переопределяет некоторые их функции
и добавляет новые. В принципе ничто не препятствует на любом
уровне разработки перейти к традиционному программированию и создавать
линейную программу, используя объекты уже существующих
классов. Следование же технологии объектно-ориентированного
программирования "до конца" предполагает, что прикладная программа
представляет собой класс самого верхнего уровня, в ее выполнение
- создание объекта этого класса или выполнение для него некоторой функции
типа "run".
Лекция 7. Виртуальные функции.
-----------------------------
7.1 Понятие виртуальной функции
------------------------------
Достаточно часто программисту требуется создавать структуры данных,
включающих в себя переменное число объектов различных типов. Для
представления их в программах используются списки или массивы ссылок на
эти объекты. Объекты разных классов имеют соответственно различные типы
ссылок, а для хранения в массиве или списке требуется один тип ссылок.
Для преодоления этого противоречия все эти классы объектов требуется
сделать производными от одного и того же базового класса, а при записи в
массив преобразовывать ссылку на объект производного класса в ссылку на
объект базового.
p[] A1
+---+ -b---------¬
¦ --------------------->-a-------¬¦======== b::f()
+---+ ¦L---------¦===¬
¦ ------------¬ L----------- ¦
+---+ ¦ C1 ¦
¦ ----------¬ ¦ -c---------¬ ¦
+---+ ¦ L-------->-a-------¬¦======== c::f()
¦ ¦L---------¦===¦
¦ L----------- ¦
¦ A1 ¦
L---------->-a-------¬ ===¦==== a::f()
L--------
class a
{ ... void f(); };
class b : public a
{ ... void f(); };
class c : public a
{ ... void f(); };
a A1;
b B1;
c C1;
a *p[3]; // Массив ссылок на объекты БК
p[0] = &B1; // Ссылки на объекты БК в
p[1] = &C1; // объектах ПК
p[2] = &A1;
Однако при таком преобразовании типа "ссылка на объект ПК" к
типу "ссылка на объект БК" происходит потеря информации о том,
какой объект производного класса "окружает" доступный через ссылку объект
базового класса. Поэтому вместо переопределенных функций в производных
классах будут вызываться функции в базовом, то
есть
p[0]->f(); // Вызов a::f()
p[1]->f(); // во всех случаях, хотя f()
p[2]->f(); // переопределены
Однако по логике поставленной задачи требуется, чтобы вызываемая
функция соответствовала тому объекту, который реально находится под
ссылкой. Наиболее просто это сделать так:
- хранить в объекте базового класса идентификатор "окружающего" его
производного класса;
- в списке или таблице хранить ссылки на объект базового
класса;
- при вызове функции по ссылке на объект базового класса
идентифицировать тип производного класса и явно вызывать для него
переопределенную функцию;
- идентификатор класса устанавливать при создании объекта ,
то есть в его конструкторе.
class a
{
public: int id; // Идентификатор класса
void f();
void newf(); // Новая функция f() с идентификацией ПК
}
a::a() // Конструкторы объектов
{ ...
id = 0;
}
b::b()
{ ...
id = 1;
}
c::c()
{ ...
id = 2
}
void a::newf()
{
switch (id)
{
case 0: a::f(); break;
case 1: b::f(); break;
case 2: c::f(); break;
}
}
p[0]->newf(); // Вызов b::f() для B1
p[1]->newf(); // Вызов c::f() для C1
p[2]->newf(); // Вызов a::f() для А1
Отсюда следует определение виртуальной функции. Виртуальная функция
(ВФ) - это функция, определяемая в базовом и наследуемая или
переопределяемая в производных классах. При вызове ее по ссылке на
объект базового класса происходит вызов той функции, которая соответствует
классу объекта, включающему в себя данный объект базового класса.
Таким образом, если при преобразовании типа "ссылка на ПК" к типу
"ссылка на БК" происходит потеря информации об объекте производного класса,
то при вызове виртуальной функции происходит обратный процесс неявного
восстановления типа объекта.
Реализация механизма виртуальных функций заключается в создании
компилятором таблицы адресов виртуальных функций (ссылок).
Такая таблица создается для базового класса и для каждого включения
базового класса в производный. В объекте базового класса создается
дополнительный элемент - ссылка на таблицу адресов его виртуальных
функций. Эта ссылка устанавливается конструктуром при создании объекта
производного класса. При вызове виртуальной функции по ссылке на объект
базового класса из объекта берется ссылка на таблицу функций и из
нее берется адрес функции по фиксированному смещению. Ниже иллюстрируется
реализация этого механизма (подчеркнуты элементы, создаваемые неявно
компилятром).
class A
{
------> void (**ftable)(); // Ссылка на таблицу адресов
// виртуальных функций
public:
virtual void x();
virtual void y();
virtual void z();
A();
~A();
};
// Таблица адресов функций класса А
------> void (*TableA[])() = { A::x, A::y, A::z };
A::A()
{
------> ftable = TableA; // Установка таблицы для класса А
}
class B : public A
{
public:
void x();
void z();
B();
~B();
};
// Таблица адресов функций класса A
// в классе B
--> void (*TableB[])() = { B::x, A::y, B::z };
¦ L переопределяется в B
B::B() L------ наследуется из A
{
--> ftable = TableB; // Установка таблицы для класса B
}
void main()
{
A* p; // Ссылка p базового класса A
B nnn; // ссылается на объект производp = &nnn;
// ного класса B
реализация
p->z(); ------------------> (*(p->ftable[2]))();
}
p nnn TableB B::z()
-----¬ -------->--B-----¬ ----->---------¬ --->----------¬
¦ ------ ftable¦--A---¬¦ ¦ 0+--------+ ¦ ¦ ¦
L----- ¦¦ ------ 1+--------+ ¦ ¦ ¦
¦+-----+¦ 2¦ --------- L---------
¦¦ ¦¦ L--------
7.2 Абстрактные классы
---------------------
Если базовый класс используется только для порождения производных
классов, то виртуальные функции в базовом классе могут
быть "пустыми", поскольку никогда не будут вызваны для объекта
базового класса. Такой базовый класс называется абстрактным. Виртуальные
функции в определении класса обозначаются следующим образом:
class base
{
public:
virtual print() =0;
virtual get() =0;
}
Естественно, что определять тела этих функций не требуется.
7.3 Множественное наследование и виртуальные функции
---------------------------------------------------
Множественным наследованием называется процесс создания производного
класса из двух и более базовых. В этом случае производный класс наследует
данные и функции всех своих базовых классов.
Существенным для реализации множественного наследования является
то, что адреса объектов второго и т.д. базовых классов не совпадают с
адресом объекта производного и первого базового классов,
то есть имеют фиксированные смещения относительно начала объекта:
class d : public a,public b, public c { };
d D1;
pd = &D1; // #define db sizeof(a)
pa = pd; // #define dc sizeof(a)+sizeof(b)
pb = pd; // pb = (char*)pd + db
pc = pd; // pc = (char*)pd + dc
D1
pd -------------------->-d---------¬
pa --------------------->-a-------¬¦T T
¦¦ ¦¦¦ ¦ db = sizeof(a)
¦L---------¦¦ +
pb --------------------->-b-------¬¦¦ dc = sizeof(a) + sizeof(b)
¦L---------¦¦
pc --------------------->-c-------¬¦+
¦L---------¦
¦ ¦
L----------
Преобразование ссылки на объект производного класса к ссылке
на объект базового класса требует добавления к указателю текущего
объекта this соответствующего смещения (db,dc), обратное преобразование -
вычитание этого же смещения. Такое действие выполняется
компилятором, когда в объекте производного класса наследуется
функция из второго и т.д. базового класса, например при определении в
классе "b" функции "f()" и ее наследовании в классе "d" вызов D1.f() будет
реализован следующим образом:
this = &D1; // Адрес объекта производного класса
this = (char*)this + db // Адрес объекта класса b в нем
b::f(this); // Вызов функции в классе b со своим
// объектом
Рассмотрим особенности механизма виртуальных функций при
множественном наследовании. Во-первых, на каждый базовый класс в
производном классе создается своя таблица виртуальных функций (в
нашем случае - для "a" в "d", для "b" в "d" и для "c" в "d").
Во-вторых, если функция базового класса переопределена в производном, то
при вызове виртуальной функции требуется преобразовать
ссылку на объект базового класса в ссылку на объект производного,
то есть для второго и т.д. базовых классов вычесть из this соответствующее
смещение. Для этого транслятор включает соответствующий код,
корректирующий значение this в виде "заплаты", передающей управление
командой перехода к переопределяемой функции.
class a
{
public: virtual void f();
virtual void g();
};
class b
{
public: virtual void h();
virtual void t();
};
class c : public a, public b
{ // f(),t() наследуются
public: void g(); // g() переопределяется
void h(); // h() переопределяется
}
a A1;
b B1;
c C1;
pa = &A1;
pb = &B1;
pa->f(); // Вызов a::f()
pb->h(); // Вызов b::h()
pa = &C1;
pb = &C1;
pa->f(); // Вызов a::f()
pa->g(); // Вызов c::g()
pb->h(); // Вызов c::h()
pb->t(); // Вызов b::t()
Таблицы виртуальных функций для данного примера имеют вид:
A1
-a----¬ Таблица ВФ для "a"
¦ ------------>--------¬
+-----+ ¦a::f() ¦
L------ +-------+
¦a::g() ¦
L------- B1
-b----¬ Таблица ВФ для "b"
¦ ------------>--------¬
+-----+ ¦b::h() ¦
L------ +-------+
¦b::t() ¦
L------- C1
T --c-----¬ Таблица ВФ для "a" в "c"
¦ ¦--a---¬¦ --------¬
db ¦ ¦¦ ----------->¦a::f() ¦
¦ ¦L------¦ +-------+
+ ¦--b---¬¦ ¦c::g() ¦
¦¦ -------¬ L------- ¦L------¦ ¦ Таблица ВФ для
"b" в "c"
¦ ¦ ¦
¦ ¦ L--->--------¬ "Заплата" для c::h()
L-------- ¦ xxx()----->--xxx()----------------¬
+-------+ ¦ this=(char*)this - db¦
¦b::t() ¦ ¦ goto c::h ¦
L-------- L----------------------
Другим вариантом решения проблемы является хранение необходимых
смещений в самих таблицах виртуальных функций.
7.4. Виртуальные базовые классы
------------------------------
В процессе иерархического определения производных классов
может получиться, что в объект производного класса войдут
несколько объектов базового класса, например
class base {}
class a : public base {}
class b : public base {}
class c : a, b {}
В классе "c" присутствуют два объекта класса base. Для исключения
такого дублирования объект базового класса должен быть
объявлен виртуальным
class a : virtual public base {}
class b : virtual public base {}
class c : public a, public b {}
a A1;
b B1;
c C1;
Объект обыкновенного базового класса располагается, как правило, в
начале объекта производного класса и имеет фиксированное
смещение. Если же базовый класс является виртуальным, то требуется его
динамическое размещение. Тогда в объекте производного
класса на соответствующем месте размещается не объект базового
класса, а ссылка на него, которая устанавливается конструктором.
Для вышеприведенного примера имеем
A1 B1 C1
--a------¬ --b-----¬ --c---------------¬
¦ ------¬ ¦ ------¬ ¦ --a-------¬ ¦
+--------+ ¦ +-------+ ¦ ¦ ¦ -------¬ ¦
¦ ¦ ¦ ¦ ¦ ¦ ¦ +---------+ ¦ ¦
¦-base--¬s); // второй объект к классу строк
} // (переход от БК к ПК)
//-------------------------------------------------------char
*string::NAME() // Возвращает имя класса строк
{ return("Строка"); }
//-------------------------------------------------------base
*string::COPY() // Создание копии объекта
{ // без копирования значения
string *p = new string; //
return(p); //
}
//-------------------------------------------------------string::operator
long() // Преобразование к типу long {
// возвращает длину строки
return (sz);
}
//-------------------------------------------------------string::operator
char*() // Преобразование к типу char* {
// возвращает текстовое представchar *p = new char[sz]; // ление
значения объекта
strcpy(p,s);
return(p);
}
//--------------------------------------------------------base&
string::operator+(char* two) // Операция "+ строка"
{ // Конкатенация строки в объекте
char ss[80]; // и входной строки
strcpy(ss,s); //
strcat(ss,two);
delete s;
s = new char[sz = strlen(ss)+1];
strcpy(s,ss);
return(*(base*)this); // Возвратить неявную ссылку на объект
} // вложенного базового класса
//-------------------------------------------------------
Базовый класс "base" необходим исключительно для обеспечения
идентичного доступа к любому элементу базы данных независимо от
его класса. Это абстрактный класс, содержащий объявление всех
вышеперечисленных функций и операций виртуальными.
class base
{
public:
virtual int GET()=0; // Ввод значения объекта
virtual void PUT()=0; // Вывод значения объекта
virtual int CMP(base*)=0; // Сравнение значений объектов
virtual char *NAME()=0; // Возвращает имя класса
virtual base *COPY()=0; // Возвращает копию объекта
virtual operator long()=0; // Преобразование к типу long
virtual operator char*()=0; // Преобразование к типу char*
virtual base& operator+(char*)=0;
// Операция "+ строка"
virtual ~base(); // Виртуальный деструктор для
// разрушения объекта ПК по
}; // ссылке на БК
Сама двумерная таблица объектов организована традиционным
для структур переменной размерности способом:
- элемент БД создается в динамической памяти при добавлении
строки к БД;
- строка БД представлена массивом ссылок на объекты класса
base. Сам массив также создается в динамической памяти при добавлении новой
строки в БД;
- ссылки на строки собраны в массив, который создается
конструктором базы данных и заполняется при вызове функции добавления
строки (таблица строк БД);
- объект класса БД (table) содержит ссылку TBL на таблицу
строк.
Особо следует остановиться на способе назначения столбцам
типов содержащихся в них элементов БД (или классов объектов). Это
делается при помощи строки заголовка БД - head. Этот массив содержит
ссылки на объекты, классы которых идентифицируют типы элементов в
соответствующих столбцах. При создании новой строки БД
виртуальной функцией COPY создаются копии объектов из строки заголовка БД,
для которых затем вызывается виртуальная функция ввода значений GET.
Строка заголовка создается конструктором объекта класса БД.
Имеется меню типов элементов, которое представляет собой массив
ссылок (TYPE) на объекты классов string,integer,dat и т.д.. Экранное меню
строится при помощи вызова виртуальной функции вывода
имени класса TYPE[i]->NAME(). После выбора строки меню ссылка на
соответствующий выбранный объект переносится в строку заголовка БД.
class table
{
int nc; // Количество столбцов
int nr; // Количество строк
char **names; // Имена стробцов
base **head; // Строка объектов заголовка БД
// для объявления типов объектов
base ***TBL; // Таблица строк БД
public:
void append(); // Добавление строки в БД
void sort(int); // Сортировка по значениям столбца
long averrage(int); // Подсчет среднего арифметического
// для столбца
base& operator()(int,int);
// Выбор объекта из БД
table(); // Конструктор - создание БД
~table(); // Деструктор - удаление БД
}
объект БД
TBL Массив строк БД
--¬ ---------¬0
¦------->+--------+.. Элемент БД
L-- +--------+i Строка БД string
base*** ¦ ----------->---------¬0 integer
+--------+ +--------+.. real
+--------+ +--------+j --dat-------¬
base** ¦ -------------->-base-----¬¦
+--------+ ¦L----------¦
base* ¦ ¦
L-----------
base
head Строка заголовка БД
--¬ S0
¦-------------->---------¬0 -string---¬
L-- ¦ ------------------>-base---¬¦
base** +--------+ ---------->L--------¦
¦ --------------¬ L--------- +-------
-+ ¦ ¦ D0
¦ --------- ¦ -dat------¬
+--------+ L--->-base---¬¦
base* ¦L--------¦
L---------//----------------------
--------------------------------// Меню классов объектов (типов
столбцов)
string S0;
dat D0;
time T0;
integer I0;
base *TYPE[] = {
(base*) &S0;
(base*) &D0;
(base*) &T0;
(base*) &I0;
};
//-----------------------------------------------------// Создание
структуры БД
#define MAXCOL 30
#define MAXREC 1000
table::table()
{
int i,j,n;
char ss[80];
names = new char*[MAXCOL]; // Таблица адресов имен столбцов
head = new base*[MAXCOL]; // Таблица ссылок на объекты
for (nc=0; ncNAME() );
}
//------ выбор типа столбца - n
head[nc] = TYPE[n]; // Ссылка на объект с классом,
// соответствующим классу
// объектов столбца
TBL = new base**[MAXREC];
nr = 0; // Таблица ссылок на строки БД
}
}
//------------------------------------------------------// Деструктор
БД
tabe::~table()
{
int i,j;
for (i=0; iCOPY();
printf("Столбец %s типа %s :",names[i],head[i]->NAME());
// Вывод подсказки имени и типа столбца
while(TBL[nr][i]->GET() ==0);// Ввод значения нового объекта
}
nr++;
}
//-------------------------------------------------------// Нахождение
среднего арифметического по заданному столбцу
long table::averrage(int n)
int i;
//-------------------------------------------------------// Сортировка
по заданному столбцу методом "пузырька"
void table::sort(int n)
{
int i,k;
base *p;
do
{
for (i=0; i< nr-1; i++) // Виртуальная функция сравнения
// объектов в соседних строках
// n-го столбца
if (TBL[i][n]->CMP(TBL[i+1][n]) class vector
{
int tsize; // Общее количество элеметов
int csize; // Текущее количество элементов
T **obj; // Массив ссылок на параметризован
// ные объекты типа "T"
public:
T *operator[](int);
// оператор [int] возвращает ссылку
// на параметризованный объект
// класса "T"
void insert(T*); // функция включения объекта типа "T"
int extract(T*); //
};
Данный шаблон может использоваться для порождения
объектов-векторов, каждый из которых хранит объекты определенного типа. Имя
класса при этом составляется из имени
шаблона "vector" и имени типа данных (класса), который подставляется вместо
параметра "Т":
vector a;
vector b;
extern class time;
vector c;
Заметим, что транслятором при определении каждого
вектора с новым типом объектов генерируется описание нового
класса по заданному шаблону (естественно, неявно в процессе
трансляции):
class vector
{
int tsize;
int csize;
int **obj;
public:
int *operator[](int);
void insert(int*);
int index(int*);
};
Далее следует очевидное утверждение, что элементыфункции шаблона
также должны быть параметризованы, то есть
генерироваться для каждого нового типа данных. Действительно, это так:
элементы-функции шаблона классов в свою
очередь также являются шаблонными функциями с тем же самым
параметром. То же самое касается переопределяемых операторов:
--- параметр шаблона - класс "T", внутренний
¦ тип данных
¦ --- имя элемента-функции или
¦ ¦ оператора - параметризовано
¦ ¦
template T* vector::operator[](int n)
{
if (n >=tsize) return(NULL);
return (obj[n]);
}
template int vector::index(T *pobj)
{
int n;
for (n=0; n::operator[](int n)
{
if (n >=tsize) return(NULL);
return (obj[n]);
}
int vector::index(int *pobj)
{
int n;
for (n=0; n class FIFO
{
int fst,lst; // Указатели на начало-конец
// очереди
T queue[size]; // Массив объектов класса "T"
// размерности "size"
public:
T from(); // Функции включения-исключения
void into(T); //
FIFO(); // Конструктор
};
template FIFO::FIFO()
{
fst = lst = 0;
}
template T FIFO::from()
{
T work;
if (fst !=lst)
{
work = area[lst++];
lst = lst % size;
}
return(work);
}
template void FIFO::into(T obj)
{
area[fst++] = obj;
fst = fst % size;
}
Пример использования:
FIFO a;
FIFO b;
struct x {};
FIFO c;
Пример сгенерированного компилятором класса для объекта "a".
class FIFO
{
int fst,lst;
double queue[100];
public:
double from();
void into(double);
FIFO();
};
FIFO::FIFO()
{
fst = lst = 0;
}
double FIFO::from()
{
double work;
if (fst !=lst)
{
work = area[lst++];
lst = lst % 100;
}
return(work);
}
void FIFO::into(double obj)
{
area[fst++] = obj;
fst = fst % 100;
}
Страницы: 1, 2
|