Загрузить архив: | |
Файл: 240-1677.zip (45kb [zip], Скачиваний: 23) скачать |
ВВЕДЕНИЕВ ОБЪЕКТНОЕ ПРОГРАММИРОВАНИЕ
Лекция 1. Объектное программирование как технология программирования
-------------------------------------------------------------
Традиционная технология программирования 70-х годов - структурное программирование:
- модульное программирование;
- нисходящее программирование;
- структурное проектирование процедур и данных (программирование без goto).
Язык Паскаль- соответствует указанным принципам и был разработан под влиянием идей структурного программирования.
Альтернативный подход - восходящее программирование -предполагает в простейшем случае создание слоя структур данных и процедур,обеспечивающих полный набор действий над объектами, которые представлены в данной задаче.Пример традиционного подхода библиотека стандартных функций.
Следующий шаг - введение в программу объектов.Под объектом понимается структура данных, которая содержит полную информацию о состоянии соответствующего физического объекта, который отображаетсяпрограммой.В Сиэтомуможет соответствовать структура struct),в Паскале - запись (record). Множество объектов одного типа составляют понятие класса. Объектно-ориентированный подход к разработке программ предполагает, что в программе устанавливается взаимно-однозначное соответствиемеждуфизическими объектами,
отображаемыми программой,и програмнными объектами, являющимися, посуществу,структурированнымипеременными (в дальнейшем под термином "объект" будем понимать программный объект).
Традиционный подход:---------- переменная тип данных
Объектно-ориентиро- физический программный класс
ванный подход: объект объект объектов
При создании объектов программист определяет множество функций,при помощи которых (а точнее,исключительно через которые)над объектом выполняется некоторое допустимое множество операций.
Такие функции должны иметь обязательный параметр - ссылку натекущий объект,для которого они вызываются. Сами функции являются неотъемлимой частью понятия класса объектов,так как они определяютвозможные действия над объектами одного и того же типа (то есть класса).
Объектно-ориентированные программы можно разрабатыватьис помощью традиционных языков программирования. Рассмотрим пример определения объектов типа "дата" на классическом Си.
//------ структура dat - аналог класса объектов "дата" --------typedef struct dat
{
unsigned day;
unsigned month;
unsigned year;
}
DAT;
//----- набор функций для класса объектов "дата" --------------staticint mm[] = {31,28,31,30,31,30,31,31,30,31,30,31};
//----- Проверкана корректность -----------------------------int TestData(p)
DAT *p;
{
if (p->month ==2 && p->day==29 && p->year %4 ==0) return(1);
if (p->month ==0 || p->month >12 ||
p->day ==0 || p->day >mm[p->month])
return(0);
return(1);
}
//------ Следующая дата ----------------------------------------void NextData(p)
DAT *p;
{
p->day++;
if (p->day <= mm[p->month]) return;
if (p->month ==2 && p->day==29 && p->year %4 ==0) return;
p->day=1;
p->month++;
if (p->month !=13) return;
p->month=1;
p->year++;
}
//------- Следующая дата через n дней --------------------------void PlusData(p,n)
DAT *p;
int n;
{
while (n-- !=0) NextData(p);
}
//------- Основная программа ---------------------------------
void main()
{
DAT a;
do
{
scanf("%d%d%d", &a.day, &a.month, &a.year);
}
while(TestData(&a) ==0);
PlusData(&a, 17);
}
//--------------------------------------------------------
Фактически определение класса объектов как типа данных и известного набора функций для выполнения операцийнад переменными этоготипа эквивалентно понятию базового типа данных (БТД) языка программирования. Единственное отличие класса от БТД заключается в том,что первый определяется программистом, а второй встроен в определение языка программирования.
Язык программированияСи++ представляетсобойрасширение языка Си для программирования объектов иих классов.Приэтом использование классов эквивалентно вплоть до синтаксиса использованию базовых типов данных:
Понятия классическогоСи Понятия Си++
------------------------- ----------- БТД: Класс:
элемент данных языка, для определяемая пользователем
которого известно множество структура, элементы которой
значений, форма представления, являютсяранее определен набор операций. ными типами данных и классами,
и множества функций,оперирующих с ним.
--------------------------------------------------------- Переменная: Объект:
область памяти, содержащая переменная, содержащая
структуру данных определенногоструктуру данных, определенную
типа. как класс.
--------------------------------------------------------- Операция: Переопределение операторов:
операция над переменной интер- функция, определенная для объек претируется по отношению к томутов указанного класса может быть
БТД, к которому относится пере-вызвана в виде одной из стандарт менная (так операция '+' ных операций языка Си, которая
по-разному интерпретируется дляпереопределяется, если операндом
переменных типа int и double). ее является объект класса, а не
переменная БТД.
Лекция 2. Дополнительные возможности языкаСи++
-----------------------------------------------
Ниже рассмотрим средства,расширяющие классический Си. Хотя
они и не относятся непосредственно к классам,с их помощью можно
реализовать рассмотренные выше принципы объектно-ориентированного
программирования.
2.1. Присваивание структур
------------------------- Операция присваивания может быть применена к структурамодного типа.В этом случае предполагается их побайтное копирование
одной в другую.Она (а не ссылка на нее) может быть также фактическимпараметром и результатом функции.Если имеется ссылка на
структуру с именем p,то результатом операции *p является структура в целом. Таким образом, структура приближается к базовым типам данных в том смысле,что над ней возможны вышеуказанные операции. Дляобозначенияструктуры можно также использовать имя
структуры без ключевого слова struct.
struct dat
{ int day,month,year; }
dat NextDat(dat x) // Формальный параметр - структура
{ ... return(x); } // Возвратить структуру как результат
dat Nextdat1(dat *p)
{ ... return(*p);} // Возврат структуры косвенно по ссылке
dat a,b,c,*q; // Ключевое слово struct не используется
void main()
{
q = &b;
a = b; // Прямое присваивание структур
a = *q; // Косвенное присваивание по ссылке
c = NextDat(b);// Присваивание структуры как результата
c = NextDat1(&b); // функции, фактический параметр в
} // NextDat - копия структуры
2.2. Обращения по адресу (неявная ссылка)
----------------------------------------
При работе со структурами большого размера - при передаче их
вкачестве параметров и результатов функций - копирование их является неэффективной операцией.Гораздо эффективнеепередавать ссылку на эту структуру.Для того,чтобы постоянно не указывать операции взятия адреса и косвенного обращения поссылке вСи++ введентип- неявная ссылка:при определении переменной неявно вводится ссылка, указывающая наэтупеременную. Использование этой переменной в большинстве операций предполагает косвенное обращение по соответствующей ссылке.При инициализации такой переменной значением другой переменной создается ссылка на эту другую переменную.При использовании в любом выражении переменной - неявнойссылки реально производится косвенное обращение по созданной ссылке.
Си++ Эквивалент в "классическом" Си
------------------------ -----------------------------//--------------- Инициализация константой-----------------int &a = 5; int a, *pa =a;
*pa = 5;
//--------------- Инициализация переменной -----------------int x; int x,*pa;
int &a = x; pa = &x;
a = 5; *pa = 5;
//-------------- Неявная ссылка на структуру ----------------structdat struct dat
{ int day,month,year }; { int day,month, year };
datx; datx;
dat& b = x; dat* pb = &x;
dat& c = {12,12,1990}; datcc = {12,12,1990};
dat *pc = &cc;
b.year = 1990; pb->year= 1990;
c.day=b.day+3; pc->day = pb->day+3;
c = b; // Копированиеpc->day = pb->day;
// структуры pc->month = pb->month;
pc->year= pb->year;
Наиболее часто неявные ссылки используются при передачепараметров и результатов функций.В этом случае транслятор сам выбирает, что необходимо использовать в качестве фактического параметра- переменную или ссылку на нее,и что используется в качестве результата - ссылка или переменная, косвенно адресуемая по ссылке. Цель подобных ухищрений будет видна позднее - при переопределении операторов, а пока можно заметить, что вызов функций, с
параметрами - обычными значениями и неявными ссылками - синтаксически идентичен. То же самое касается результатов.
В качестве иллюстрации рассмотрим три примера функций, имеющих в качестве формального параметра и результата структуру,которая передается соответственно:
- значением;
- явной ссылкой;
- неявной ссылкой.
Пример 1. Параметры - значения
---------------------------------------------------------dat Inc(dat x) ========> - копирование
{ --------> - ссылка
x.day++;
return(x); ----¬ стек +---+x.day++
} ¦ b =========> x =========¬
L---- +---+ ¦ return(x)
void main() ¦
{ ----¬ стек +---+ --¦-¬ временная
dat a,b,*p; ¦ a <========= x <======= ¦ переменная
a = Inc(Inc(b)); L---- +---+ L---p = &Inc(b); x.day++
a = *p;
}
Пример 2. Параметры - явные ссылки
---------------------------------------------------------dat* Inc(dat* x)x->day++
{ x->day++
x->day++; ----¬ стек +---+
return(x); г===== b <--------- x ¦
} ¦--> <----¬ +-¦-+
¦¦ L---- ¦ ¦return(x)
¦¦ г======+=====void main() a=*..¦ ¦ ¦ ¦ +---+ стек
{ ¦¦ --¦-¬ L---- x ¦
dat a,b,*p;¦ ¦ ¦ =========> ¦
a = *Inc(Inc(&b)); ¦¦ L---- +-¦-+
p = Inc(&b); ¦¦ ----¬ ¦return(x)
a = *p; ¦L-- <===========} ¦ L--- ¦ ----¬
L====> a ¦
L---
Пример 3. Параметры - неявные ссылки
---------------------------------------------------------dat& Inc(dat& x)x.day++ неявная ссылка dat* px
{ x.day++
x.day++; ----¬ стек +---+
return(x); г===== b <--------- px¦
} ¦--> <----¬ +-¦-+
¦¦ L---- ¦ ¦return(px)
¦¦ г======+=====void main() a=*..¦ ¦ ¦ ¦ +---+ стек
{ ¦¦ --¦-¬ L---- px¦
dat a,b,*p;¦ ¦ ¦ =========> ¦
a = Inc(Inc(b)); ¦¦ L---- +-¦-+
p = &Inc(b); ¦¦ ----¬ ¦return(px)
a = *p; ¦L-- <===========} ¦ L--- ¦ ----¬
L====> a ¦
L---
Сравнение этих примеров показывает следующее:
- при работес формальнымпараметром- неявнойссылкой используетсяимя формального параметра в качестве идентификатора переменной,которая заменяется транслятором на косвенное обращение по неявной ссылке;
- при возвращении результатаиспользуется имяпеременной,которая заменяется транслятором неявной ссылкой на нее;
- примеры 2 и 3 идентичны по реализации,ноотличаются по синтаксису вызова функции;
- примеры 1 и 3 отличаются по реализации,ноидентичны по синтаксису вызова функции;
- из предыдущего следует, что при вызове функции список фактический параметровнедостаточендля определения транслятором способа их передачи (значением или ссылкой),поэтому в Си++для каждой внешней функции необходимо задать прототип.
Так как размер структуры, передаваемой в качестве результата функции, может быть сколь угодно большим, то для его хранения необходимосоздать временную переменную.Транслятор "Borland C" в этом случае поступает следующим образом:
- при входев функцию в стеке предполагается существование неявного параметра - "длинной" ссылкина структуру,вкоторой размещается результат функции;
- при выполнении операции return(x), где x - локальная переменная или формальный параметр, выполняется побайтовое копирование переменной x по адресу, заданному неявным параметром;
- если результат функции непосредственно присваивается другой переменной-структуре,то при вызове такой функции в стек помещается неявныйпараметр- ссылка на переменную в левой части операции присваивания;
- во всехостальных случаях в вызывающей функции создается по одной неявной автоматической переменной на каждый вызовфункции,возвращающей структуру в качестве результата,а при вызове передается соответствующая ссылкана этупеременную-структуру. Такие переменные имеют все свойства автоматических, они существуют все время работы вызывающей функции,возможно дажеполучить ссылку на такую переменную.
Программа на Си++ Реализация
----------------- ---------- -- неявный параметр
dat Inc(dat x) void Inc(dat *r,dat x)
{ {
x.day++; x.day++;
return(x); *r = x; // Копирование
} } // результата
void main() void main()
{ {
dat a,b*p; dat a,b,*p;
dat t,u; // Временнye переменнye
a = Inc(b); Inc(&a,b); // Ссылка на левую часть
p = &Inc(b); Inc(&t,b); // Присаивание временной
p = &t; // переменной и получение
a = *p; a = *p; // ссылки на нее
a = Inc(Inc(b)); Inc(&u,b); // Промежуточный результат
Inc(&a,u); // во временной переменной
} }
2.3. Функции - элементы структуры
-------------------------------- Повторим рассмотренный выше пример в другой форме:
//------------ структура dat - аналог класса объектов "дата" --struct dat
{
unsigned day;
unsigned month;
unsigned year;
int TestData();
void NextData();
void PlusData(int n)
{
while(n-- !=0) dat::NextData(this);
}
};
//----------- набор функций для класса объектов "дата" --------staticint mm[] = {31,28,31,30,31,30,31,31,30,31,30,31};
//----------- Проверкана корректность -----------------------int dat::TestData()
{
if (month ==2 && day==29 && year %4 ==0) return(1);
if (month ==0 || month >12 || day ==0 || day >mm[month])
return(0);
return(1);
}
//----------- Следующая дата ----------------------------------void dat::NextData()
{
day++;
if (day <= mm[month]) return;
if (month ==2 && day==29 && year %4 ==0) return;
day=1;
month++;
if (month !=13) return;
month=1;
year++;
}
//--------- Основная программа --------------------------------void main()
{
dat a;
do
{
scanf("%d%d%d", &a.day, &a.month, &a.year);
}
while(a.TestData() ==0);
a.PlusData(17);
}
//------------------------------------------------------- Как видно из примера,в качестве элементов структуры могут
выступать функции. Такие элементы-функции имеют следующие особенности:
- тело функцииможет бытьопределенов самойструктуре (функция PlusData). В этом случае функция имеет стандартный вид;
- в определении структурыдается толькопрототипфункции(заголовок с перечислением типов формальных параметров).Определение самой функции дается отдельно,при этом полное имя функции
имеет вид
<имя структуры>::<имя функции>
- в теле фукции неявно определен один формальный параметр с
именем this - ссылка на структуру, для которой вызывается функция
(В нашем примере это будет struct dat *this ). Поля этой структуры доступны через явное использование этой ссылки
this->month = 5;
this->day++;
или неявно
month = 5;
day++;
- для переменной,имеющей тип некоторойструктуры,вызов
функцииэлемента этой структуры имеет вид
<имя переменной>.<имя функции> ( <список параметров> )
2.4. Переопределение функций
--------------------------- В Си++ возможно определение нескольких функций с одинаковым
именем,но с разными типами формальных параметров. При этом компиляторвыбирает соответствующую функцию по типу фактических параметров. Переопределяемую функцию необходимо объявить с ключевым
словом overload:
overload SetDat;
void SetDat(int dd,int mm,int yy,dat *p)
{ // Дата вводится в виде трех целых
p->day=dd;
p->month=mm;
p->year=yy;
}
void SetDat(char *s,dat *p)// Дата вводится в виде строки
{
sscanf(s,"%d%d%d", &p->day, &p->month, &p->year);
}
void main()
{
dat a,b;
SetDat(12, 12, 1990, &a); // Вызов первой функции
SetDat("12,12,1990", &b); // Вызов второй функции
}
Функции-элементы также могут быть переопределены,приэтом явного объявления не требуется.
structdat
{
int day,month,year;
void SetDat(int,int,int);
void Setdat(char *);
}
void dat::SetDat(int dd,int mm,int yy)
{
day=dd; month=mm; year=yy;
}
void dat::SetDat(char *s)
{
sscanf(s,"%d%d%d",&day,&month,&year);
}
void main()
{
dat a,b;
a.SetDat(12,12,1990);
b.SetDat("12,12,1990");
}
2.5. Операторы управления динамической памятью
---------------------------------------------
В библиотекеСи имеются две функции управления динамической памятью - malloc() и free(), которые выделяют и освобождают областьпамяти заданного размера (в байтах).В этой области программа может разместить переменную (или массив), котораяназывается динамической. При выделении памяти под динамическую переменную необходимо при помощи операции sizeofопределятьколичество байтов,необходимое для размещения переменной указанного типа. В Си++ введены два оператора,аналогичные функциям malloc и free new и delete.Они отличаются от соответствующих функций тем, что допускают использованиявкачестве аргументовнепосредственно спецификацию типа создаваемой динамической переменной и ссылки на динамическую переменную:
Си++ "Классический" Си
------------------------- ---------------------------------char *s,x[80]; char *s,x[80];
dat *p,*q; struct dat *p,*q;
void main() void main()
{ {
p = new dat; p = malloc(sizeof (struct dat));
q = new dat[15]; q = malloc(15*sizeof (struct dat));
gets(x); gets(x);
s = new char[strlen(x)+1]; s = malloc(strlen(x)+1);
... ...
deletep; free(p);
deleteq; free(q);
deletes; free(s);
}
Операторы имеют вид:
<результат: ссылка на <абстрактный
динамическую переменную>new описатель типа>
delete<ссылка на динамическую
переменную>
2.6. Параметры функций по умолчанию
----------------------------------
При определенииформальных параметровфункцииможет быть
указано его значение,принимаемое при вызовепоумолчанию при
отсутствии этого параметра в списке фактических:
//----- Функция устанавливает по умолчанию текущее значение года,
//----- месяца и дня
#include
void dat::SetDat(int d=0, int m=0, int y=0)
{
structdate x;
getdate(&x);// Стандартная функция получения
// текущей даты
// Проверка на значение по умолчанию
year = (y == 0) ? x.da_year : y;
month= (m == 0) ? x.da_month: m;
day= (d == 0) ? x.da_day: d;
}
2.7 Контроль преобразования типов ссылок
---------------------------------------
В "классическом"Сипри выполнении присваивания,передаче фактических параметров происходит автоматическоепреобразование ссылок к базовым типам данных (int,unsigned) и наоборот,а также преобразование одного типа ссылки к другому.В Си++ такие "вольности" исключены, программист должен сам выполнить явное преобразование.Например, при использовании функции распределения динамической памяти, имеющей прототип в "alloc.h"
extern void* malloc(int n);
dat *p;
p = (dat *) malloc (10*sizeof(dat));
¦
L--- преобразование void*в dat*
Естественно, чтоэто преобразованиетипов фиктивное в том смысле,что не меняет значения ссылки и не приводит кгенерации кода.Онотолько меняет"точкузрения" транслятора на данную ссылку.
2.8 Вставляемые (inline) функции
-------------------------------
Если функция(обычная илиэлемент-функцияструктуры или класса) объявлены inline-функциями, то при вызове такихфункций транслятор выполняет подстановку по тексту программы тела функции с соответствующей заменой формальных параметровнафактические. Элемент-функция также считается inline по умолчанию, если ее тело определено непосредственно в определении структуры (иликласса),например:
struct dat
{
int d,m,y;
void Setdat(char *p)// Функция inline по умолчанию
{
... // Тело функции
}
2.9 Ссылки на элементы структуры
-------------------------------
Если структура имеет несколько элементов одного типа,тодля нееможет бытьсоздана "внутренняя" ссылка,которая принимает значение внутреннего адреса (смещения) элемента относительно выбранной структуры.Формирование и использование такой ссылки ясно
из примера:
structdat
{
int day,month,year;
void Getdat();
void Putdat();
void Nextdat();
}
int dat::*p; // Ссылка на элемент типа int
// в структуре dat
p = & dat::month; // Значение p - смещение (адрес)
// элемента month в структуре типа
// dat
dat x,*px = &x; //
x.*p = 5; // Обращение по внутренней ссылке
px->*p
= 5; //
// <*dat>-> *<ссылка на элемент>
Эквивалентно
x.month = 5;
px->month =5;
Аналогичная внутренняя ссылка может быть создана для элементов-функций, принадлежащих однойструктуре,при этомфункции
должны быть идентичными по результатам и параметрам:
void (dat::*fun)(); // Ссылка на элемент-функцию
// структуры dat
fun = & dat::Putdat(); // Значение fun - ссылка на
// элемент-функцию Putdat в dat
(x.*fun)(); // Вызов элемента-функции по
(px->*fun)(); // ссылке fun для структуры x
// и для структуры по ссылке px
Эквивалентно
x.Putdat();
px->Putdat();
2.10 Неизменяемые переменные (константы)
---------------------------------------
В Си++ введен дополнительный контроль за изменением значений переменных. Ключевое слово const, используемой при определении и инициализации переменной, запрещает ее изменение, что контролируется транслятором при ее дальнейшем использовании.Такая же возможностьсуществует и для формальных параметров функции,например:
const int n=5;
n++; // Запрещено
int xxx(const int m)
{
m++; // Запрещено
}
Применительно к ссылке const может использоваться в двух вариантах, применительно к самой ссылке (адресу) и применительно к указуемому значению:
- при использовании conts применительно к указуемому значению разрешается модифицировать саму ссылку при помощи присваивания и операций адресной арифметики, а изменения операнда косвенно по ссылке запрещены.Такая ссылка называется ссылкой на постоянный объект:
const char * p;
p = "1234567890";// Разрешено присваивание ссылке
p + =3; // Разрешена модификация ссылки
*(p+3) = '3'; // Запрещено присваивание по ссылке
(*p)++;// Запрещен инкремент по ссылке
- при использовании const применительно к ссылке запрещается менять значение ссылки после инициализации, в том числе средствами адресной арифметики. Такая ссылка называется постоянной ссылкой на объект:
char const* p = "1234567890";
char c;
(*p) = '3'; // Разрешено присваивание по ссылке
p++; // Запрещено изменение значения
c = *(p+3); // самой ссылки
Полная фиксацияссылки и адресуемого ею объекта возможна в виде
const char const* p = "1234567890";
2.11 Общие замечания о дополнениях в Си++
----------------------------------------
Основные отличия Си++ от "классического" Си:
- структура (struct) приближена по свойствам к базовым типам
данных (char,int);
- введено понятие элемента-функции.Элементы-функции играют
роль своеобразного "интерфейса"дляиспользования определенной
программистом структуры;
- расширены возможности транслятора по контролю и преобразованию параметров при вызове функции (неявная ссылка,переопределение, параметры по умолчанию). Именно поэтому вызову любой внешней функции должно предшествовать объявление ее прототипа (заголовка функции со списком типов параметров).
Все эти новые свойства необходимыпри определениипонятий
класса и объекта.
Лекция 3. Классы. Объекты. Конструкторы и деструкторы
----------------------------------------------------
3.1.Понятие класса и объекта в Си++
----------------------------------
В самом простом виде класс определяется в Си++ какструктура,работа сэлементамикоторой возможна только через элементы-функции.В отличие от структуры класс имеет "приватную" (личную) часть, элементы которой не могут быть доступны иначе как через другие элементыфункции, и "публичную" (общую) часть, элементы
которой могут быть использованы непосредственно. Объектом называется определяемая в программе переменная,тип которойопределен
как класс (структура):
Определение структуры Определение класса
------------------------- -----------------------------------structdat class dat
{ { // Приватная часть
int day,month,year; int day,month,year;
public: // Публичная часть
voidSetDat(int,int,int); void SetDat(int,int,int);
voidSetDat(char *); void SetDat(char *);
} аа}
voidmain() void main()
{ {
// Опред-ние переменных a,b // Опред-ние объектов a,b класса dat
dat a,b; dat a,b;
a.day = 5; // Непосредственное использование
a.month = 12; // приватной части объекта запрещено
bAA.SetDat("12,12,1990"); b.Setdat("12,12,1990");
} }
"Приватная" частькласса не обязательно должна следовать в начале определения класса.Для ееобозначенияв произвольном местеопределения классаможноиспользовать служебноеслово private.
Tаким образомв первомприближениикласс отличаетсяот структуры четко определенным интерфейсом доступа к его элементам.
Объекты класса обладают всеми свойствами переменных, в том числе такими, какобластьдействия икласспамяти (время жизни).
Последнее свойство наиболее интересно,так как процессы создания иуничтожения объектов класса могут сопровождаться вызовом функций (конструктор и деструктор).Напомним,что по классам памяти (и времени жизни) в Си различаются переменные:
- статические (внешние),создаваемые в статическойпамяти программыи существующие в течение всего времени работы программы;
- автоматические,создаваемые в стеке в момент вызова функции и уничтожаемые при ее завершении;
- динамические, создаваемые и уничтожаемые в свободной памяти задачи в моменты вызова функций malloc() и free() или выполнения операторов new и delete.
Соответственно в программе возможно определение статических, автоматических и динамических объектов одного класса:
class dat
{ ....... }
dat a,b;// Статические объекты
dat *p; // Ссылка на объект
void main()
{
dat c,d;// Автоматические объекты
p = new dat;// Динамический объект
...
delete p; // Уничтожение динамического объекта
} <---------------------// Уничтожение автоматических объектов
3.2. Созданиеи уничтожение объектов.
Конструкторы и деструкторы
-------------------------------------
Процесс создания и уничтожения объектов класса ассоциируется
при объектном программировании с созданием и уничтожениемсоответствующихим физических объектов.Поэтому с этими действиями
необходимо связывать определяемые программистом функции для установкиначальных значений,резервирования памяти и т.д.. Неявно
вызываемые функции при создании и уничтожении объектов класса называютсяконструкторамии деструкторами.Они определяются как
элементы-функции класса и имена ихсовпадаютс именемкласса.
Конструкторовдля данного класса может быть сколь угодно много,
если они отличаются формальными параметрами, деструктор же всегда
один и имеет имя ~<имя класса>.
С процессом создания объектов связано понятие их инициализации. Инициализировать объекты обычным способом нельзя. Их инициализация осуществляется либо явнымприсваиванием(копированием)
другого объекта, либо неявнымвызовом конструктора.Если
конструктор имеет формальные параметры, то в определении переменнойпосле ее имени должны присутствовать в скобках значения фактических параметров.
Момент вызова конструктора и деструктора определяется временем создания и уничтожения объектов:
- для статическихобъектов- конструктор вызывается перед
входом в main(), деструктор - после выхода из main(). Конструкторывызываются в порядке опредлеления объектов, деструкторы - в
обратном порядке;
- для автоматическихобъектов - конструктор вызывается при
входе в функцию (блок), деструктор - при выходе из него;
- для динамических объектов - конструктор вызывается при выполнении оператора new, деструктор -привыполнении оператора delete.
В Си++ возможно определениемассива объектовкласса.При этом конструкторидеструктор вызываются для каждого элемента массива и не должны иметь параметров.Привыполнении оператора deleteдля ссылки на массив объектов необходимо также указывать его размерность. Конструктор для массива объектов должен быть без параметров.
//--------------------------------------------------------#include
#include
staticint days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
class dat
{
int day,month,year;
public:
dat(int,int,int);// Конструктор с параметрами
// (возможно умолчание)
dat(char *); // Конструктор с одним параметром
dat(); // Конструктор без параметров
~dat();// Деструктор
};
//------- Конструктор с параметром - текстовая строка ---------dat::dat(char *s)
{
inti;
char ss[80];
strcpy(ss,s);
for (i=0; ss[i] !=0; i++)
if (ss[i]=='-') ss[i]=','; // Замена '-' на ','
sscanf(ss,"%d%d%d",&day,&month,&year);
}
// Конструктор с тремя параметрами (по умолчанию 0 - текущая дата)
dat::dat(int d=0, int m=0, int y=0)
{
structdate x;
getdate(&x);// Стандартная функция получения
// текущей даты
// Проверка на значение по умолчанию
year = (y == 0) ? x.da_year : y;
month= (m == 0) ? x.da_month: m;
day= (d == 0) ? x.da_day: d;
}
//------ Конструктор без параметров --------------------------dat::dat()
{
structdate x;
getdate(&x);// Стандартная функция получения
// текущей даты
year = x.da_year ;
month= x.da_month;
day= x.da_day ;
}
//------ Деструктор ------------------------------------------dat::~dat()
{
printf("Дата ==> %2d-%2d-%4dn",day,month,year);
}
//------------------------------------------------------dat a("12-12-1990"); // Внешняя переменная - конструктор
// вызывается перед main()
dat b[10]; // Массив объектов - конструктор без
// параметров вызывается перед main()
void xxx(dat &p)
{
dat c(12,12); // Вызывается Конструктор dat(int,int,int)
// для автоматического объекта
dat d = p; // Конструктор для автоматического объекта не
... // вызывается, т.к. объект инициализируется
... // копированием
} // При выходе из функции вызываются деструкторы
// для объектов c и d
void main()
{
int i,n;
scanf("%d",&n);
dat *p = new dat[n]; // Создание массива динамических объектов // конструктор без параметров явно вызывается
for (i=0; i<10; i++) // n раз
xxx(b[i]); // При вызове неявно передается ссылка на b[i]
for (i=0; i< n; i++) //
xxx(p[i]) // При вызове неявно передается ссылка на p[i]
delete[n] p; // Уничтожение массива динамических объектов // деструктор явно вызывается n раз
} // Деструктры для a и b[10] вызываются после
// выхода из main()
}
3.3 Ограничение доступа к объектам класса. Дружественность.
----------------------------------------------------------
Выше было дано формальное определение класса. Содержательная его сторона состоит в том, что класс выступает в Си++ как определенный программистом тип данных.Такой тип данных включает в себя:
- описание структуры объектов класса. Объект класса представляет собой совокупность элементов (структуру),каждый из которыхявляетсяпеременной некоторого типа или объектом ранее определенного класса;
- описание множества допустимыхоперацийнад объектом класса, которые представлены элементами-функциями;
- описание процедурсозданияи уничтожения объекта класса (конструктор и деструктор).
При этом внутренняя структура объектов (описанная в "приватной" части класса) доступна только элементам-функциям.Наоборот, "публичная" часть определения класса -этоописание тойчасти объекта,ккоторой возможендоступ из любого места программы, если доступен сам объект.Данное ограничение позволяет исключить некорректное использование объектов класса,как случайное, так и умышленное. Возможные ошибки, связанные с неправильным "поведением" объектов класса, локализуются таким образом в определении самого класса, а не в использовании его объектов.
Иногда требуются исключения из этого правила,когда к "приватной"частиобъекта класса требуется доступ из некоторой отдельной функции,либо из всех элементов-функций другогокласса, либо из переопределяемой в другом классе операции.Тогда в определении класса,к объектам которогоразрешается такойдоступ, должно бытьобъявлениефункции или другого класса "дружественным".Это согласуется с тем принципом,что сам класс определяет права доступа к своим объектам "со стороны".
Объявление дружественнойфункции предствляет собой прототип функции, объявление переопределяемой операции или имя класса, которым разрешается доступ, с ключевым словом friend впереди. Общая схема объявления такова:
class A
{
int x; // Приватная часть класса
...
friendclass B; // Функции класса B дружественны классу A
// (имеют доступ к приватной части A)
friendvoid C::fun(A&);
// Элемент-функция fun класса C имеет
// доступ к приватной части A
friendvoid xxx(A&,int);
// Функция xxx дружественна классу A
friendvoid C::operator+(А&);
// Переопределяемая в классе C операция
// <объект C>+<объект A> дружественна
// классу A
class B
{
public: int fun1(A&);// Необходим доступ к приватной части A
void fun2(A&);// ----------------------------------- }
class C
{
public: void fun(A&);// ------------------------------------ void operator+(A&);//------------------------------- ....
}
К средствам контроля доступа относятся также объявления элементов-функций постоянными (const).В этом случае элементфункция
не имеет права изменять значение текущего объекта, с которым она
вызывается. Заголовок функции при этом имеет вид
void dat::put() const
{
}
3.4 Статические элементы класса
------------------------------
Иногда требуется определитьданные, которыеотносятсяко
всем объектам класса. Это требуется, если объекты класса разделяют некоторый общий ресурс,связаны в общий список и т.д.. С этой
целью в определении класса могут быть введены статические элементы - переменные.Такой элемент сам в объекты классане входит, затоприобращении к нему формируется обращение к внешней переменной с именем
<имя класса>::<имя элемента>
соответствующего типа. Доступность ее определяется стандартнымобразомв зависимостиот размещения в приватной или общей части класса. Сама переменная должна быть явно определена в программе и инициализирована.
Пример: объекты класса связаны в односвязный список
--------------------------------------------------class list
{
staticlist *fst; // Ссылка на первый элемент
staticlist *lst; // Ссылка на последний элемент
list *next; // Ссылка на следующий элемент
.... .....
public:
void insfst(); // Вставить в начало списка
void inslst(); // Вставить в конецсписка
void show(); // Просмотр всех объектов
void extract(); // Исключть из списка
list(); // Конструктор
~list(); // Деструктор
}
list list::fst=NULL;// Определение статических элементов
list list::lst=NULL;
//-------------------------------------------------------void insfst()
{
next = NULL;
if (fst==NULL)
fst=lst=this;
else
{ next=fst; fst=this; }
}
//-------------------------------------------------------void inslst()
{
next = NULL;
if (fst==NULL)
fst=lst=this;
else
{ lst->next=this; lst=this; }
}
//-------------------------------------------------------void list::extract()
{
list *p,*pred; // Поиск текущего и предыдущего
for (pred=NULL,p=fst; p !=NULL; // в списке
pred=p,p=p->next)
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
{ ...
staticvoid show(); // Стaтическая функция просмотра
} // всего списка объектов
//-------------------------------------------------------staticvoid 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
staticint 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
frienddat operator+(dat,int)// Операции с явной передачей
frienddat 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; i<10; i++)
p[i] = p[i] + i;
delete[10] p;
}
Многие из переопределяемых операций представляют собой аналог некоторого конвейера,по которому продвигается объект в процессе обработки.Такие операции в качестве одного из операндов и в качестве результата имеют объект одного класса. Вышеприведенные операциис датами как раз являются таковыми. Но преобразование объекта можетпроизводитьсяв такихоперацияхдвумя разными способами.При этомвозникаютнекоторые особенности в вызове конструкторов и деструкторов, на которых необходимо остановиться.
В рассмотренном выше примере одним изпараметров элементафункции-объект класса dat передается по значению,аналогично передается и результат операции. Эти два объекта определены неявнои создаютсяпутемкопирования из других объектов.Поэтому конструкторы для них не вызываются. Однако при уничтоженииэтих объектов производится вызов деструкторов. Таким образом, при проектировании класса и передаче его объектов по значению необходимо учитывать эти "лишние" объекты и деструкторы. Перечислим возможные случаи возникновения и уничтожения объектов в функции:
- объект -формальный параметр - создается в стеке,его значение представляет собой копию соответствующего фактического параметра, соответственно, конструктор для него не вызывается;
- автоматический объект x в теле функции создаетсявстеке перед вызовомфункции.Если он инициализируется копией другого объекта (например,текущего), то конструктор для него не вызывается:
dat x = *this;
В противномслучае вызывается конструктор с соответствующим набором параметров:
dat x;
dat x("13-JUL-1990");
- в любомслучае перед выходом из функции вызывается деструктор для автоматического объекта;
- если функция имеет формальный параметр - объект класса, то
для него вызывается деструктор после выхода из функции (еслиэто
переопределяемый оператор, то после вычисления выражения);
- если функция возврашает объект по значению (а не появной
илинеявной ссылке),то функция получает дополнительный неявный
параметр - ссылку на результат (см."Неявные ссылки"), а по оператору returnпроизводитсякопирование возвращаемого объекта в
объект-результат по заданнойссылке.Однако привызоветакой
функции транслятор может по-разному формировать эту ссылку:
- если производится прямое присваивание результатафункции,
топередается ссылка на объект в левой части операции присваивания;
- в остальных случаях на каждый вызов функции (по синтаксису
программы) вызывающей функцией формируется дополнительный автоматический объект.Конструктор для него не вызывается,так как он
формируется копированием результата функции, а деструктор вызывается при выходе из вызывающей функции, как для обычного автоматического объекта.
Наиболее эффективным способом работы с объектом является передачаего на вход (формальный параметр) и выдача на выход в качестве ссылки (преимущественно неявной).Однако вэтом способе
все изменения, производимые при выполнении операции, происходят в
исходном объекте. Это не согласуется с интерпретацией большинства
бинарныхопераций, гдерезультатявляется отдельным элементом
данных (объектов),а операнды не меняются.В качествепримера
рассмотримвариант операции сложения "дата + целое",в котором
исходная дата увеличивается на заданное значение иодновременно является результатом операции.
class dat
{
int day,month,year;
public:
void next(); // Элемент-функция вычисления
// следующего для
dat& operator+(int);// Операция "дата + целое"
// с неявным операндом через this
//------ Операция "дата + целое" ------------------------------//1. Дружественная элемент-функция с полным списком аргументов
//2. Альтернативный вариант предыдущей функции
//3. Первый операнд класса dat - неявная ссылка на фактический
// параметр, значение меняется при выполнении операции
//4. Тело функции непосредственно в определении класса.
//-------------------------------------------------------frienddat& operator+(dat& p,int n)
{
while (n-- !=0) p.next();// Вызов функции next для объекта p
// по неявной ссылке на него.
return(p); // Возврат неявной ссылки неа p
}
// Операция "целое + дата" ------------------------------------//1. Дружественная элемент-функция с полным списком аргументов
//2. Второй операнд класса dat - неявная ссылка на фактический
// параметр, значение меняется при выполнении операции.
//3. Тело функции непосредственно в определении класса.
//-------------------------------------------------------
frienddat& operator+(int n, dat& p)
{
while (n-- !=0) p.next(); // Вызов функции next для объекта p
// по неявной ссылке на него.
return(p); // Возврат неявной ссылки на p
}
//-------------------------------------------------------
dat(); // Конструкторы
dat(int,int,int);// (см. предыдущие примеры)
dat(char *); //
~dat();// Деструктор
}; // (см. предыдущие примеры)
//------ Операция "дата + целое" -------------------------------//1. Элемент-функция с неявным первым аргументом по ссылке this
//2. Меняется значение текущего объекта
//3. Результат - неявная ссылка на текущий объект
//--------------------------------------------------------dat& dat::operator+(int n)
{
while (n-- !=0)
next(); // Вызов функции next с текущим объектом this
return(*this); // Возврат неявной ссылки на объект (this)
}
//---------------------------------------------------------
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];
e = a++;
d=b+15;
for (i=0; i<10; i++)
p[i] + i;
delete[10] p;
}
Лекция 5. Особенности переопределения различных операций
-------------------------------------------------------
Одной из важных возможностей использования переменных базовых типов в операциях является их преобразование к другим типам,
котороеможет производитьсянеявно(в бинарных арифметических
операциях и при присваивании), либо с использованием операции явного преобразования типа.Если преследовать целью достижение при
добавлении новых классов всей полноты свойств базовых типовданных, тоаналогичные преобразования необходимо ввести и для этих
классов. Далее рассмотрим два возможных способа такого преобразования- стандартный,преобразование объекта класса к переменной
базового типа данных, и нестандартный - преобразование переменной
базового типа данных или объекта класса к объекту другого класса.
5.1 Преобразование к базовому типу данных
----------------------------------------
В качестве примера рассмотрим неявное преобразование объекта класса dat к базовым типам данных int и long. Сущность его заключается в вычислении полного количестваднейв дате,заданной входным объектом(long) и количества дней в текущем году в этой же дате (int).Для задания этих операции необходимопереопределить в классе dat одноименные операции int и long. Переопределяемые операции задаются соответствующимиэлементами-функциямибез параметров,ссылка на текущий объект входного класса передается через неявный параметр this.Тип результата совпадает сбазовым типом, к которому осуществляется приведение и поэтому не указывается.
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(int,int,int); //
dat(char *); //
~dat(); // Деструктор
};
//------ Преобразование dat в int -----------------------------// Используется ссылка на текущий объект this
//------------------------------------------------------- dat::operator int()
{
int r; // Текущий результат
int i; // Счетчик месяцев
for (r=0,
i=1; i r += days[month]; // месяцах if
((month>2) && (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(xxxp);
или 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,в котором одним из элементов
приватной части является дата. staticint
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к известно, определение объекта класса в
виде <имя класса> <объект 2> = <объект 1> приводит к тому, что объект
инициализируется путем побайтного копирования содержимого другого объекта без
вызова конструктура.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 <производный> : <базовый
1>,<базовый 2>,...<базовый n> { определение приватной и публичной части производного класса } Перечислим основные свойства
базового и производного классов: - объект
базовогокласса определяется в
производном классе как неименованный. Это значит, что он не может быть
использован в явном виде как обычный
именованный объект; - элементы данных базового класса
включаются в объект производного
класса(какправило,
компилятор размещает их в начале
объекта производного класса).Oднако
приватнаячастьбазового класса закрыта для прямого использования в производном классе; - элементы-функции базового класса
"наследуются" в производном классе, то есть вызов функции,
определенной в базовом классе, для объекта производного класса возможен и
понимаетсякаквызов ее
для входящего в него объекта базового класса; - в
производномклассеможно
переопределитьнаследуемую функцию,которая
будет вызываться вместо наследуемой.
При этом для выполнения
соответствующихдействийнад
объектомбазового класса она может включать явный вызов переопределяемой
функции. Пример схемы определения производного
класса class a { public: void
f() {} void
g() {} } class b : a ------------------------ базовый класс { public:
voidf() --------------------
"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--¬<----¦-base-¬<---- ¦ ¦
¦ ¦ ¦ ¦L-------¦ ¦L------¦ ¦ L---------- ¦ ¦ L--------- L-------- ¦ --b-------¬ ¦ ¦ ¦
¦ ------¬¦ ¦ ¦
+---------+¦¦ ¦ ¦
¦ ¦¦¦ ¦ ¦
L----------¦¦ ¦ ¦-base---¬<---¦ ¦ ¦L--------<---- ¦
L----------------- Таблицы виртуальныхфункций
располагаются в каждом базовом классе
обычным образом. Лекция 8. Пример использования
виртуальных функций
------------------------------------------------- Реляционная базаданных
(РБД)представляет собой таблицу, состоящую
из произвольного количества строк и столбцов. В качестве элементов, хранящихся
в клетке такой таблицы могут быть целые, вещественные
числа,строки постоянной и переменной
длины, значениядатыи
времени.Все типы элементов в
одном столбце должны совпадать.Количество строк и столбцов, названия
столбцов и типы элементоввних
заранее не известны и определяются динамически, то есть в
процессе работы программы. В нашем примере рассмотрим РБД,целиком размещенную в памяти. 8.1 Организация РБД в виде таблиц ссылок
на объекты
-------------------------------------------------- Прежде всего, определим элементы,
размещенные в клетках таблицы, как объекты соответствующих элементарных
классов: - class string- строки переменной длины; - class integer - целые числа; - class real - вещественные числа; - class dat - дата; - class time - время и т.д. Перечень таких классов может быть
расширен.Для каждогоиз нихнеобходимо определить множество функций и
операций,который будет
использоваться при работе с таблицей: ввод и вывод значения объекта,
сравнение объектов, создание копии объекта, приведение к базовым
типам данных (int,long, char*),
арифметических операций надобъектами
ицелыми и т.д..Интерпретация последних должна быть своя
для каждого типа объектов.Каждый из
этих классов сделаем производным от некоторого базового класса, роль которого
будет рассмотрена позднее. В качестве примера рассмотрим класс строк
переменной длины. class string : base { char
*s; // Ссылка на
строку int
sz; // Длина строки public:
int GET(); // Ввод строки void
PUT(); // Вывод строки int
CMP(base*); // Сравнение строк char
*NAME(); // Возвращает имя
класса строк base
*COPY(); // Возвращает
копию объекта operator long(); // Преобразование к типу long// возвращает
длину строки operator char*(); // Преобразование к типу char*// возвращает
копию строки base& operator+(char*); // Операция "+ строка" // присоединяет
строку string(); ~string(); } //------------------------------------------------------string::string() // Конструктор { s =
NULL; // Строка
пустая sz = 0; } //------------------------------------------------------string::~string() // Деструктор { if (s
!=NULL) delete s; // Освободить
память } //------------------------------------------------------int string::GET() { char
ss[80]; if (s
!=NULL) delete s; gets(ss); // Ввод строки и
размещение ее s = new
char[sz = strlen(ss)+1];// в динамической памяти strcpy(s,ss); return(1); // Ввод всегда правильный } //------------------------------------------------------void string::PUT() { puts(s); } //------------------------------------------------------int string::CMP(base* two)// Сравнение строк по алфавиту { string *p
= (string*) two; // Преобразовать
ссылку на return(strcmp(s,
p->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---------//------------------------------------------------------// Меню классов объектов (типов столбцов) stringS0; dat D0; time T0; integer
I0; base *TYPE[] = { (base*) &S0; (base*) &D0; (base*) &T0; (base*) &I0; }; //-----------------------------------------------------// Создание структуры БД #define
MAXCOL30 #define
MAXREC1000 table::table() { int i,j,n; char
ss[80]; names =
new char*[MAXCOL]; // Таблица
адресов имен столбцов head= new base*[MAXCOL]; // Таблица ссылок на объекты for
(nc=0; nc {
// Ввод имени
столбца gets(ss); if (strlen(ss)=0) break;// Пустая
строка - выход name[nc] = new char[strlen(ss)+1]; //------
построение меню типов элементов БД ... for (j=0; j<3; j++) { gotoxy(10,5+j); cputs( TYPE[j]->NAME() ); } //------
выбор типа столбца - n head[nc] = TYPE[n]; // Ссылка на объект с классом, //
соответствующим классу // объектов столбца TBL = new base**[MAXREC]; nr = 0; // Таблица ссылок на строки БД } } //------------------------------------------------------// Деструктор БД tabe::~table() { int i,j; for (i=0;
i { for (j=0; j delete TBL[i][j]; // Разрушение
объекта i-ой строки // j-го столбца по
ссылке на // объект БК // (виртуальный
деструктор) delete TBL[i]; } deleteTBL; for (j=0;
j delete names[j]; } //------------------------------------------------------// Добавление строки к БД void table::append() { int i; TBL[nr] =
new base*[nc]; // Создание таблицы ссылок для строки БД for (i=0;
i { // заголовка БД TBL[nr][i] = head[i]->COPY(); printf("Столбец %s типа %s
:",names[i],head[i]->NAME()); // Вывод подсказки
имени и типа столбца while(TBL[nr][i]->GET() ==0);// Ввод
значения нового объекта } nr++; } //-------------------------------------------------------// Нахождение среднего арифметического по
заданному столбцу long table::averrage(int n) { long r; int i; if
(n<0 || n>=nc) return(0); for (r=0,
i=0; i return(r
/ nr); } //-------------------------------------------------------// Сортировка по заданному столбцу методом
"пузырька" 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]) <0) { p = TBL[i][n]; TBL[i][n] =
TBL[i+1][n]; TBL[i+1][n] = p; k++; } } while
(k); // Пока есть
перестановки } //----------------------------------------------------------//
Выбор элемента (i,j) в БД - возвращает неявную ссылкуна // БК
объекта, по которой возможен вызов любой виртуальной //
функции staticbase
empty; // Пустой объект для
ошибочного выбора base& table::opertor()(int i,int j) { if
(i<0 || i >= nr) return(empty); if
(j<0 || j >= nc) return(empty); return
(*TBL[i][j]); // Возвратить неявную
ссылку на объект } //--------------------------------------------------------// Пример работы с классом РБД void main() { int i,j; table R; // Создание БД for (i=0;
i<10; i++) R.append(); // Ввод 10-ти строк R.sort(1); // Сортировка по первому
столбцу scanf("%d
%d", &i, &j); //
Вывод значения элемента R(i,j).PUT(); R(1,2) +
"1234"; //
Операция "объект + строка" // для элемента
1,2 } Лекция 9. Шаблоны. ----------------- Довольно часто классы создаются для
объединения множества
элементов данных, которые внутри объекта могут быть
связаны массивом ссылок, списком, деревом и т.д.. При этом
объект класса содержит ссылки Таким образом, требуется определить
некоторое множество идентичных классов с параметризованнымтипом
внутренних элементов. Тоестьдолжна
бытьзаданазаготовка класса
(шаблон), вкоторомв
видепараметразадан
тип (класс)
входящих в него внутренних элементов данных.
Тогда при
создании объекта необходимо дополнительно
указыватьи конкретный
тип внутренних элементов вкачествепараметра. Создание объекта
сопровождается также и
созданием соответствующего
конкретного класса для заданного конктретного типа. Принятый в Си++ способ определения
множества классов с
параметризованным внутренним типом данных (иначе,макроопределение)называется
шаблоном(template). Синтаксис шаблона
рассмотримнапримере
шаблонаклассавекторов, содержащих
динамический массив ссылок на переменные заданного типа.
--- параметр шаблона - класс "T", внутренний
¦ тип данных
¦ --- имя группы
шаблонных классов template
{ int tsize;//
Общее количество элеметов int csize;//
Текущее количество элементов T **obj;//
Массив ссылок на параметризован //
ные объекты типа "T" public: T *operator[](int); // оператор [int] возвращает
ссылку // на параметризованный объект
// класса "T" voidinsert(T*);//
функция включения объекта типа "T" int extract(T*); // }; Данный шаблон может использоваться для
порождения объектов-векторов,
каждый из которых хранит объекты определенного типа. Имя класса приэтом
составляетсяизимени шаблона
"vector" и имени типа данных (класса), который подставляется вместо
параметра "Т": vector vector extern
class time; vector Заметим, что транслятором при определении
каждого вектора с
новым типом объектов генерируется описание нового класса по
заданному шаблону (естественно, неявно в процессе трансляции): class
vector { int tsize; int csize; int **obj; public: int *operator[](int); voidinsert(int*); int index(int*); }; Далее следует очевидное утверждение, что
элементыфункции шаблона также должны быть параметризованы, тоесть генерироваться
для каждого нового типа данных. Действительно,
этотак:элементы-функциишаблона
классоввсвою очередь
также являются шаблонными функциями с тем же
самым параметром.
То же самое касается переопределяемых
операторов:
--- параметр шаблона - класс "T", внутренний
¦ тип данных
¦ --- имя
элемента-функции или
¦ ¦ оператора - параметризовано
¦ ¦ template
{ if (n
>=tsize) return(NULL); return
(obj[n]); } template
{ int n; for (n=0;
n if (pobj == obj[n]) return(n); return(-1); } Заметим, что транслятором при определении
каждого вектора с
новым типом объектов генерируется набор элементовфункций по заданным шаблонам
(естественно, неявно в процессе трансляции).
При этом сами шаблонные функции должны
размещатьсявтомжезаголовочном
файле,где размещается определение
шаблона самого класса. int*
vector { if (n
>=tsize) return(NULL); return
(obj[n]); } int
vector { int n; for (n=0;
n if (pobj == obj[n]) return(n); return(-1); } Шаблоны могут иметь также и
параметры-константы, которые
используются для статического
определенияразмерностейвнутренних
структурданных.Кроме
того,шаблон может
использоваться для размещениянетолько
ссылокна параметризованные
объекты, но и самиобъекты.В
качестве примера
рассмотрим шаблон для построения
циклическойочереди ограниченного
размера для параметризованных объектов. template
{ int fst,lst; //
Указатели на начало-конец // очереди T queue[size]; // Массив объектов класса "T" // размерности
"size" public: T from(); //
Функции включения-исключения voidinto(T); // FIFO(); //
Конструктор }; template
{ fst = lst
= 0; } template
{ T work; if (fst
!=lst) { work = area[lst++]; lst = lst % size; } return(work); } template
{ area[fst++]
= obj; fst = fst
% size; } Пример
использования: FIFO FIFO struct x {}; FIFO Пример
сгенерированного компилятором класса для объекта "a". class
FIFO { int fst,lst; double queue[100]; public: double from(); voidinto(double); FIFO(); }; FIFO { fst = lst
= 0; } double
FIFO { double
work; if (fst
!=lst) { work = area[lst++]; lst = lst % 100; } return(work); } void
FIFO { area[fst++]
= obj; fst = fst
% 100; }