В обектно ориентираното програмиране тази страна на динамиката, която може да се нарече динамика на съществуването, е реализирана чрез механизмите за създаване и инициализация на екземпляри на обекти
- В обектно ориентираното програмиране се използват два основни подхода за създаване на екземпляри – статичен и динамичен подход
- Статичният (автоматичен) подход се осъществява чрез използване на статични променливи (обекти):
статичните обекти съществуват през цялото време на изпълнение на програмата или подпрограмата, в която са създадени
- за тях се заделя памет при декларацията им в програмата или подпрограмата
- заделянето на памет се основава на механизма на стека (stack)
- отделената памет се освобождава (т. е. обектите се унищожават) при приключване на изпълнението на съответната програмна част
- програмистът не се грижи за заделяне и освобождаване на паметта
Динамичният подход за създаване на обекти-екземпляри се осъществява с използване на динамични променливи (обекти)
динамичните обекти могат да се създават и унищожават във всеки момент от изпълнение на програмата
- при динамичния подход (основан на механизма на динамичната памет – heap) памет за променливите се заделя и освобождава по специално указание от потребителя
- на динамичната променлива се съпоставя адрес от динамичната памет, където се разполага нейната стойност (тази връзка не е постоянна и може да се променя по време на съществуването на променливата)
- програмистът има грижата за създаване на динамични обекти (заделяне на памет) обикновено чрез специална операция New, разбира се придружена още от операцията инициализация над новосъздадения екземпляр (чрез метод-конструктор напр.)
– освобождаване на паметта, заета от динамичен обект в обектно ориентираното програмиране:
- Java, Смолток – извършва се автоматично чрез т. нар. механизъм “изхвърляне на боклука”
- BP, C++ – е грижа на програмиста, при което основна роля играе специален метод на обекта (деструктор)
– във всички ООЕП работата с динамични екземпляри се осъществява чрез използване на указатели (Смолток и Java – неявно;C ++ и Object Pascal – явно)
Предимства на ООП в следствие от възможността за създаване на динамични обекти
- Адекватност при моделиране на реални системи – във всеки момент да се създават точно необходимите по брой и тип обекти; контролира се времето на тяхното съществуване
- Икономия на памет – ненужните обекти могат да се унищожават във всеки момент от функционирането на системата, с което се освобождава памет за нови обекти
Проблеми при реализацията на динамиката на съществуване на обектите, произтичащи от възможността за наследяване
- Проблем 1: Резервиране на памет за обекти
отделяне на минимално количество статична памет – за екземпляра се резервира толкова памет (на принципа на стека), колкото е необходимо, за да се съхрани обект от неговия тип, без да се имат предвид неговите наследници. Такъв подход е предвиден в езиците C++, BP, Object Pascal
отделяне на максимално количество статична памет – когато се срещне обект от конкретен тип се резервира максималното количество памет, необходимa за да се съхрани стойност от типа на обекта или на произволен негов наследник (не се използва в никой от ЕП поради принципно заложеното в него разхищение на памет)
отделяне на динамична памет –
- отлага се процеса на заделяне на памет за екземпляра – заделя се само памет за указател към динамичната памет, където ще бъде разположен този екземпляр
- по време на изпълнение на програмата, се заделя необходимото за действителния тип на екземпляра количество памет (чрез New) –С++, Object Pascal, Java, Смолток
Проблем 2: Съвместимост на обектите
Съвместимост по отношение на:
- изпълнение на операцията присвояване
- предаване на параметри
Подходи при реализацията на съвместимостта в етапното програмиране:
- чрез копиране
- чрез указатели
Реализация на съвместимостта чрез копиране
При този подход се копират общите елементи на структурата на обектите
- Пример (ARat и AMix са екземпляри съответно от класовете TRational и TMixed):
- Особености:
При това присвояване може да настъпи загуба на информация (стойността на полето Whole)
Прилага се в ООЕП, при които заделянето на памет е на принципа за резервиране на минимално количество статична памет
Реализация на съвместимостта чрез указатели
При този подход не се извършва копиране на самата структура, а на адресите от паметта, където са разположени обектите
- Пример (указателите PRat и PMix сочат към два динамични екземпляра съответно от типовете TRational и TMixed):
- Особености:
- В резултат на присвояване между указателите PRat и PMix, те ще сочат към едно и също място в динамичната памет, т. е. към един и същи динамичен обект
- Такъв начин на работа е възможен в ЕП, в които паметта за обектите се заделя на принципа за резервиране на динамична памет
Обработката на динамични обекти С++:
- Стъпка 1: Деклариране на променлива указател към съответния клас
- Стъпка 2: Заделяне на памет за динамичните обекти (оператор new) и инициализация:
TPoint *PPoint=new TPoint; /*рез. на памет и автоматично извикване на конструктора по подразбиране*/
TVector *PVector=new TVector;
- Стъпка 3: Използване на динамичните обекти (например *PPoint и *PVector), т. е. достъп до полета и методи – чрез операцията селектор:
(*PPoint).X = 7.3; /*първи начин*/
PPoint->Y = 6.24; /*втори начин*/
cout << PPoint->Y << ‘ ’ << PVector->EndP->Y;
- Стъпка 4: Освобождаване на съответното количество динамична памет (оператор delete) и извикване на деструктор:
delete PVector; /*извикване на деструктора и освобождаване на заетата памет*/
В С++ за създаване, обработка и унищожаване на динамични обектови променливи се използва стандартният тип указател и техниката за работа с указатели
- Синтаксис за дефиниране на указател (към клас):
<име_на_тип> *<име _на_указател> <инициализация>незад.
Резервиране и освобождаване на памет за обекти:
- Пример (!!! Обърнете внимание, че ако разчитаме на автоматично генерираните конструктор и деструктор по подразбиране, то те няма да резервират и освобождават необходимото количество динамична памет за обекта *PVector):
#include <iostream.h>
class TPoint { /*точка в равнината*/
//… както в горния пример
};
class TVector: public TPoint {/*вектор в равнината*/
//… както в горния пример
};
TVector::TVector() { /*конструктор*/
EndP=new TPoint();
}
TVector::~TVector() { /*деструктор, извършващ освен
служебните си функции и някои други, като:*/
delete EndP;/*изв. на дестр. и освобождаване на EndP*/
/*извикване деструктора на предшественика*/
}
void main() {
TPoint *PPoint=new TPoint;
TVector *PVector=new TVector;
(*PPoint).X = 7.3; /*първи начин*/
PPoint->Y = 6.24; /*втори начин*/
cout << PPoint->Y<<’ ‘<<PVector->EndP->Y;
delete PVector; //изв. на дестр. и осв. на заетата памет
}
Съвместимост и сравняване на обекти
В С++ може да се избира как да резервира памет за обектите – по механизма за резервиране на минималното количество статична памет (в стека) или чрез използване на динамична памет
TPoint АPoint; //статична памет
TVector АVector;
TPoint *PPoint=new TPoint; //динамична памет
TVector *PVector=new TVector;
- Присвояване на обекти (правилото за съвместимост бе формулирано по-рано, !!!но присвояването е възможно само при публично наследяване):
APoint = AVector; //правилно
Структурата на обекта AVector ще бъде копирана в структурата на обекта APoint, но това не означава инициализация на екземпляра APoint – ето защо извикването, например, на метода Print (APoint.Print ();) ще бъде неправилно – изпълнява се версията от TPoint
Присвояване на указатели към обекти
– Правило:
На променлива – указател към клас може да се присвои само променлива – указател към същия тип клас или към кой да е негов наследник
– Пример:
PPoint = PVector; //присвояване на указатели към обекти
//или директно
PPoint = new TVector;
След присвояването, указателят PPoint сочи към адреса в паметта, където е разположен вече инициализираният обект *PVector. Ако след това бъде извикан методът Print (PPoint->Print ();), то правилно ще бъде изпълнена реализацията на Print за типа TVector
Предаване на параметри на функции
Правило: Фактическият параметър, съответен на формален параметър от тип клас, може да бъде от същия тип клас или от типа на кой да е негов наследник
Предаване на параметри по стойност – стойността на фактическия параметър се копира на ново място в паметта (в стека) è еквивалентно на присвояване чрез копиране
Предаване на параметри по адрес – в стека на подпрограмата се съхранява адресът (т. е. указателят) на фактическия параметър, а не негово копие è действията са аналогични на присвояването чрез указател
Пример:
void ParamValue(TPoint& PlaneObj) { //параметър по адрес
PlaneObj.Print();
};
//…
ParamValue(APoint); ParamValue(AVector);ê
Във всеки от горните два случая процедурата ще изпълнява реализацията на метода Print, която съответства на поведението на фактическия параметър
Сравняване на обекти
Стандартно в С++ обекти могат да се сравняват единствено чрез указатели, т. е. сравняват се техните адреси в паметта
if (PPoint == PVector) …
!!! В повечето практически случаи не е достатъчно сравняване на обектите само по адреси, защото те може да са разположени на различно място в паметта, но да имат еднакво вътрешно състояние. В такъв случай програмистът може да предефинира някои от операторите за сравнение (аналогично на предефиниране на оператора за присвояване)
Особености
– Проблем:
Нека имаме PPoint = new TVector;
Въпреки, че сме сигурни, че PPoint сочи към динамичен обект от тип вектор, то следните оператори са недопустими:
PVector = PPoint;
cout << PPoint->EndP->Y;
– Решение на проблема:
С помощта на оператора за типизиране (type casting) на указатели:
cout << ((TVector*)PPoint)->EndP->Y <<endl;//типизиране1
PVector = (TVector*)PPoint; //типизиране 2
– !!!Безразборното използване на техниката за претипизиране може да доведе до грешки
Копиращи конструктори
Когато стандартните средства копиране на обекти не са достатъчни, то за съответния клас може да се наложи деклариране на т.н. в С++ копиращ конструктор
– Синтаксис – има само един аргумент, който е псевдоним (reference) на класа, на който принадлежи конструктора (ако има повече аргументи, то те трябва да имат стойности по подразбиране)
– Семантика – създава копие на вече съществуващ обект от този клас
– Забележка: Ако класът не декларира копиращ конструктор, то компилаторът се опитва да създаде такъв (той е публичен и извършва копиране на членовете данни на класа, като ако е възможно, извиква и техните конструктори) и използва при нужда него
Предефиниране на оператора за присвояване
Когато стандартнoтo присвояване на обекти не e удовлетворително, то може да се наложи да се използват възможностите на С++ за предефиниране на оператора за присвояване
Синтаксис – синтаксисът за предефиниране на оператори в С++
Забележки: Ако класът не предефинира оператора за присвояване, то компилаторът използва стандартно поелементно копиране; операторът за присвояване може да се предефинира само като член-функция на клас
Активиране – при присвояване
Пример:
class TPoint { /*точка в равнината*/
public:
float X,Y;
TPoint() {X=0;Y=0;}; /*конструктор*/
TPoint(TPoint &O) {X=O.X+1;Y=O.Y+1;}; //copy констр.
TPoint& operator=(TPoint&); //присвояване
};
TPoint& TPoint::operator=(TPoint& O){ //деклариране
X=O.X+5;Y=O.Y+5; return *this;
};
//…
TPoint APoint;
//активиране на copy констр.
TPoint BP=APoint; cout << BP.Y<<’ ‘<<BP.X<<endl;
// присвояване
APoint =BP; cout << APoint.Y<<’ ‘<<APoint.X<<endl;
Сходни статии:
- Свързване на съобщения и методи в C++. Механизъм на «ранното» свързване Стартирането на метод като реакция на определено съобщение трябва да се извършва в съответствие с природата на обекта-получател на съобщението, независимо от това, че този метод може многократно да е...
- Повторение и рекурсия в ПРОЛОГ 1. ПРОГРАМИРАНЕ НА ПОВТАРЯЩИ СЕ ОПЕРАЦИИ Съществуват два начина за реализиране на правила, които да изпълняват една и съща задача многократно: 1. Повторение. 2. Рекурсия. В първия...
