Дәріс мақсаты: амалдарды қайта анықтау, операторларды қайта жүктеу түсініктерімен танысу және операторларды қайта анықтаудың түрлерін, қайта анықталған операторларды шақыру формаларын бөліп көрсету.
Кілттік сөздер: қайта анықтау; жай функция; компонентті функция; операторлы форма; стандартты форма; унарлы операторлар; бинарлы операторлар; меншіктеу операторлары.
Жоспар:
- Операторларды қайта анықтау.
- Операторларды қайта анықтаудың түрлері.
- Унарлы операторларды қайта анықтау.
- Бинарлы операторларды қайта анықтау.
- Меншіктеу операторларын қайта анықтау
- New және delete операторларын қайта анықтау
- Операторларды қайта анықтау.
С++ тілінде стандартты анықталатын типтерге қолданылатын операторлар кірістірілген болып табылады. Яғни программист бұл оператордың орындалуына әсер ете алмайды. Дегенмен, С тілі кез- келген операторды жаңа құрылатын кластар үшін қайта анықтау мүмкіндігін қарастырады. Оператор қайта анықталған кезде оның бастапқы мәндерінің бірде бірі жоғалмайдыб тек қана класс объектілері үшін ұқсас мәнді оператор енгізіледі.
Операторларды қайта анықтау арнайы operator@ атымен жүзеге асырылган функцияның ерекше түрі. Мұндағы, @ белгісі қайта анықталатын оператор белгісі, мұндай функциялар әдетте функция операторлар деп аталады. Функция операторлар класс сипаттамасының ішінде және одан тыс анықтама алады. Сондықтан олардың келесі түрлерін ажырат:
- жай(қарапайым) яғни кластан тыс анықталатын функция оператор. Ол бір орынды(бір аргументтен тұратын) және екі орынды(екі аргументтен тұратын) болады.
- Компонентті, яғни класта анықталған функция оператор. Онда бір аргумент әрқашан класс объектісі болады.
Функция операторының анықталуының жалпы формасы кестеде келтірілген. Кестеде келтірілген сипаттамада @ белгісі кез-келген мүмкін болатын операция <нәтиже типі> қайтарылатын мән типі (көбіне класс типіне сәйкес келеді, бірақ басқа тип блуы да мүмкін)
Операторларды қайта анықтау кезінде төмендегілерді ескеру кажет.
- *, sizeof, ?, :, #, ##, ::, class:: операторларын қайта анықтауға болмайды.
- =, [ ], ( ) – операторлары тек қана класс құрамында қайта анықтала алады.
- Өайта анықталған = операторы туынды кластардан мұрагерленбейді.
- Операторлардың преаритетін және ассоциотивтігін өзгертуге болмайды.
Функция операторларды сипаттау формалары.
|
Жай функция |
Компонентті функция |
|
бірорынды <нәтиже типі> operator@(аргумент) |
Екіорынды <нәтиже типі>operator@ () |
|
екіорынды <нәтиже типі>operator@(арг1,арг2) |
екіорынды <нәтиже типі>operator@(аргумент2) |
Функция оператордың шақырудың екі формасы бар кстандартты және операторлы.Олар келесі кестеде келтірілген.
Функция-операторларды шақыру формалары.
|
Стандартты форма |
Оператор форма |
|
Жай функция үшін operator@(<аргумент>) operator@(<арг1><арг2>) |
Жай функция үшін @<аргумент> <аргумент1>@< аргумент2> |
|
Компонент функция үшін <аргумент>.operator@() <арг1>.operator@(<арг2>) |
Компонент функция үшін @<аргумент> <аргумент1>@<аргумент2> |
Кластан тыс функция операторды сипаттаған кезде оған тек кластың ортақ компоненттері ғана қолжетіледі. Егер функция операторға кластың кез-келген компоненттеріне қатынау мүмкіншілігіне ие болу қажет болса, онда оны friend спецификаторымен осы класқа достық функия ретінде анықтау қажет.
Класс ішінде функция операторды сипаттағанда бірінші аргумент әрқашан класс объектісі болып табылады, бұл оны қолданудың мүмкіндіктеріне біршама шектейді.
Мысалы:компонентті функция операторды сипаттау.
#include <string.h>
#include<stdio.h>
#include<iostream.h>
#include<conio.h>
class sstr // жол класы
{ private: //класстың жасрын жолдары.
Char *str;
Int len;
Public: //класстың баршаға қолжетілу жолдары.
Void print (void)
{ cout <<” жасырын жолдардың ішіндегісі:” << endl;
cout<<”жол”<<str<<endl;
cout<<”жол ұзындығы:”<<len<<endl; }
str(){ }
sstr(char *vs)
{ len =strlen(vs); str-new char[len+1]; strcpy(str,vs); }
~sstr() {delete str;}
Sstr & operator +(sstr&); /*екіорынды операцияны жүзеге асыратын компонентті функцияның прототипі”+” */
Sstr & operator-(); /*бірорынды операцияны жүзеге асыратын компонентті функцияның прототипі”-” */
sstr & sstr::operator-()
{ char c;
for (int i=0; i<=len/2-1; i++) {c=str[i]; str[i]=str[len-1]; str[len-i-1]=c;}
return *this; }
sstr & sstr ::operator ++(sstr &A)
{ int l-A.len+len; char *s; s=bew char[l+1]; strcpy(s,str);
strcat(s,A.str); len=l; str=s; return *this; }
void main()
{ clrscr();
Sstr aa(“қолдану мысалы”);
sstr bb(“операцияларды қайта анықтау”);
sstr cc=aa+bb; //шақырудың операторлық формасы
cc.print();
sstr dd; dd=aa.operator+(bb); //шақырудың қалыпты формасы
dd.print();
-aa; -bb; // унарлы минустың қолданылуы-жолдарды қарату
aa.print(); getch(); }
Кейбір операцияларды олардың комутативтілігін қамтамасыз ету үшін қайта анықтау класс денесіндегі функция-операторлар-дың бірнеше варианттарын сипаттау керек.
С++ енгізу-шығару кітапханасының кластарымен жұмыс жасауда енгізу-шығару кластары “<<” (ағымға қосылу) және “>>” (ағымнан шығару) операцияларын мәліметтердің тек қана қалыпты типтеріне ғана қолданады. Сондықтан жолдар типін белгілі-бір қолданушы кластар үшін де, және тұтас кластар үшін де бұл класс объектілерін шығарған кезде “<<” және ”>>” операцияларын әрбір класс үшін қайта анықтаған жөн.
“Ағымға қосылу” және “ағымнан шығару” операцияларын қайта анықтауда операцияларды сипаттаудың келесі формасы қолданылады:
Operator & operator<<(ostream & out,<жаңа тип><аты>)
{<функция-оператор денесі>};
Istream & operator >>(istream & in,<жаңа тип>&<аты>)
{<функция-оператор денесі>}.
Бұл операцияларға private және proected сипатталған жолдарды кірістіру кезінде “<<” және “>>” операцияларын серіктестік ретінде сипаттаған дұрыс.
Жасырын түрде меншіктеп алу операциясы кез-келген классқа анықталған және көшірмелейтін конструкторда жасырын түрде қолданылатынға ұқсас объектілерді көшірмелеуді қамтамасыз етеді. Керек жағдайда бұл операцияны да қайта анықтауға болады. Кейбір жағдайларда қайта анықтау міндетті түрде керек, мысалы динамикалық жолдармен жұмыс жасау кезінде.
- Операторларды қайта анықтаудың түрлері.
С++ көптеген операциялар жұмысын нақтылы класс обьектілеріменқолданған кезде олар берілген функцияларды орындайтындайетіп кйта анықтауға мүмкіндік береді. Бұл өзіндік типтерді дәл стандартты типтердей қолдануға көмектеседі. Өзіндік операциялар белгілерін енгізуге болмайды. С++ те бар кез-келген операторларды қайта анықтауға болады, тек мынадан басқа:
. .* ?: : : # ## sizeof
Операторларды қайта анықтау арнайы түрдегі әдістер көмегімен жүзеге асырылады және келесі ережелерге сүйенеді:
- Операторларды қайта анықтау кезінде аргументтер көлемі, мәліметтердің қалыпты типтерінде қолданылатын ассоциациялар ережелері ( оңнан солға қарай және солдан онға қарай) және операциялар приоритеттері сақталады;
- Мәліметтердің стандартты типтері үшін операторларды қайта анықтауға болмайды;
- Функция-операторлар жасырын түрде қойылатын аргументтерге ие емес;
- Функция-операторлар мұраға беріледі (= басқасы);
- Функция-операторлар static сияқты анықтала алмайды;
Функция-операторларды үш түрлі тәсілмен анықтауға болады: ол не класс әдісі болу керек, немесе класстың серіктес функциясы, немесе қарапайым функциясы болу керек. Сонғы екі жағдайда функция класстың типін, бағыттаушысын класс сілтемесіне ие ең болмаса бір аргументті қабылдау керек.
Функция-оператордың operator деген кілттік сөзі болады, одан кейін қайта анықталатын операторының белгісі жүреді:
Тип operator оператор (параметлер тізімі) {функция денесі }
- Унарлы операторларды қайта анықтау
Класстың ішінде анықталатын унарлы функция-операторлар параметрлерсіз тұрақты емес әдістің көмегімен көрсетілуі керек, бұл кезде операнд болып оны шақырған обьект табылады, мысалы:
Class monstr {
…
monstr & operator ++() {++health: return *this}
};
monstr Vasia:
cout << (++Vasia).get_health():
Егер функция класстан тыс анықталатын болса, ол класс типінің бір параметріне ие болу керек:
Class monstr {
…
Friend monstr & operator ++(monstr &M):
}; r
monstr& operator ++(monstr &M) {++M.health: return M:}
Егер класс ішіндегі функцияны серіктестік ретінде сипаттамасақ, өзгертілетін жолдар қолжетімділігін ескеру қажет. Берілген жағдайда health жолы private спецификаторымен сипатталғандықтан сыртқа қолжетімсіз, сондықтан оны өзгерту үшін сәйкес әдісті қолдану қажет. сипаттауға monstr класының health жолының мағынасын өзгіртуге мүмкіндік беретін change_health әдісін енгіземіз:
void change_health(int he) { health =h: }
Сол кезде инкремент операторын кластан тыс сипатталған қарапайым функция көмегімен қайта анықтауға болады:
monstr& operator ++(monstr &M) {
int h = M.get_health(); h++
M.chenge_health(h);
return M;
}
Постфикстілі инкремент және декремент операторлары int типінің бірінші параметріне ие болу керек. Ол тек қана оларды префиксті түрден ажырату үшін қолданылады:
Class monstr{
…
monstr operator ++(int) {
monstr M(*this): health++
return M;
}
};
monstr Vasia;
cout << (Vasia++).get_health();
- Бинарлы операторларды қайта анықтау
Класс ішінде анықталатын бинарлы функция-операторлар параметрлерсіз тұрақты емес әдістің көмегімен көрсетілуі керек, бұл кезде оны шақырған обьект бірінші операнд болып табылады:
Class monstr {
…
bool operator > (const monstr &M) {
If (health> M.health) return true;
Return false;
}
};
Егер функция класстан тыс анықталатын болса, ол класс типінің екі параметріне ие болу керек:
bool operator >(const monstr &M1,const monstr &M2) {
if(M1.get_health() > M2.get_health()) return true;
return false;
}
- Меншіктеу операторларын қайта анықтау
Меншіктеу операторы жасырын түрде кез-келген класста элемент бойынша көшіру ретінде анықталған. Бұл операция әркез бір обьектіге екіншісінің мағынасын меншіктеген кезде шақырылады. Егер класста жады динамикалық түрде көрсетілетін жолдарға ие болса, онда меншіктеудің өзіндік операциясын анықтау керек. Меншіктеу семантикасын сақтап қалу үшін оператор-функция сілтемені ол шақырылған обьектіге қайтарып және параметр ретінде жалғыз ғана аргумент- меншіктелетін обьектіге сілтемені қабылдау керек.
const monstr& operator = (const monstr &M) {
/ / Өзіне меншіктеп алуды тексеру:
if (&M == this) return *this;
if (name) delete [ ] name;
if (M.name) {
name = new char [strlen(M.name) + 1];
strcpy(name, M.name); }
else name = 0;
health = M.health: ammo = M.ammo; skin =M.skin;
return *this;
}
Функциядан сілтегіштің обьектіге қайтуы меншіктеу операторының тізбегін жасайды:
monstr A(10), B, C;
C =B = A;
Меншіктеу операторын тек класс әдісі ретінде ғана анықтауға болады. Ол мұраға берілмейді.
- New және delete операторларын қайта анықтау
Жадыны басқарудың балама нұсқаларын қамтамасыз ету үшін обьектіге динамикалық жадыны және сәйкесінше обьект массивтерін бөлуге new және new [ ] операторларының , сонымен қатар delete және оны босату үшін delete [] операцияларының өзіндік нұсқаларын анықтауға болады. Бұл функция-операторлар келесі ережелерге сай болуы тиіс:
- Оларға класс типінің параметрлерін беру қажет емес;
- new және new [ ] функцияларына бірінші параметр болып size_t типті обьектінің көлемі жіберілуі тиіс ( бұл sizeof операторымен қайтарылатын тип, ол <stddef. h> тақырыптық файлда анықталады); шақырған кезде ол функцияларға түсініксіз түрде жіберіледі;
- return сілтегішті өзге типтерге қайтарып тұрса да, олар void* мәнінің қайтарылатын типімен анықталуы керек;
- delete операторы void қайтарымының типіне және void* типінің бірінші аргументіне ие болу керек;
- Жадыны бөлу және босату оперторлары класстың статистикалық элементтері болып табылады.
Қайта анықталған операторлар әрекеті олармен жасырын түрде орындалатын әрекеттерге сай келуі тиіс. New операторы үшін бұл ол дұрыс мағынаны қайтаруы керек, нөлдік көлемдегі жадты бөлуге сұранысты корректілі түрде өңдеп және сұранысты орындау мүмкіншілігі болмаған кеэде шығарып тастап отыруды білдіреді. Delete операторы үшін нөлдік сілтегішті жою қауіпсізболуы керек екендігін ескерген жөн, сондықтан оператор ішінде сілтегішті нөлге және тепе-теңдік жағдайындағы кез-келген әрекеттің жоқ екендігіне тексеру жүргізу керек. Жадыны бөлудің және босатудың қалыпты операторлары класстың әрекет облысында қайта анықталғандармен бірге қолданылуы мүмкін ( көрінетін облысқа кіретін операторлар көмегімн : : (осы класс және өзгелері үшін де).
Жадыны бөлу операторын қайта жүктеу жадыны үнемдеу үшін,бағдарламаның тез жұмыс жасауы және мәліметтерді кейбір белгілі бір облыстарға орнату үшін қолданылады. Мысалы, кейбір обьектіге сілтегіші бар класс сипатталсын:
class Obj { … };
class pObj {
…
private:
Obj *p;
};
p0bj типіндегі обьект астына new қалыпты операторының көмегімен жадыны бөлу кезінде
pObj *p = new pObj;
байттардың нақтылы көлемі sizeof(p0bj) асып кетеді, себебі new , әдетте бөлінетін облыстың басына оның көлемін жазып кетеді ( delete операторларын дұрыс өңдеу үшін).
Шағын обьектілер үшін бұл накладты шығындар маңызды болуы мүмкін. Жадыны үнемдеу үшін жадының үлкен блогын бөліп отыратын, кейін онда 0bj сілтегіштерді орнататын өзіндік p0bj классты new операторын жазуға болады. Бұл үшін p0bj обьектісіне head0free статикалық жолы енгізіледі, онда келесі обьектіні орнатуға арналған блоктың бірінші бос ұяшығына сілтегіш сақталады
Қолданылмайтын ұяшықтар тізімге жиналады. Байланыс жолының астында жадты алмау үшін, бірлестік қолданылады ( union), оның көмегімен бір ұяшық не обьектіге сілтеуішті орнату үшін, не келесі бос ұяшықпен байланыс үшін қолданылады:
class pObj {
public:
static void * operator new(size_t size);
…
private:
union {
Obj *p / / обьектіге сілтеуіш
pObj *next; / /келесі бос ұяшыққа сілтеуіш
};
static const int BLOCK_SIZE; / / блок колемі
// бос ұяшықтар тізімінің атауы
static pObj *headOfFree;
};
void * pObj: :operator new(size_t size) {
// жадының дұрыс емес көлемінің сұранысын қайта бағыттау
//new қалыпты операциясы
If (size != sizeof(pObj)) return : :operator new(size);
pObj *p = headOfFree; // бірінші бос ұяшыққа сілтегіш
//бос ұяшықтар тізімінің сілтегішінің орнын өзгерту
If (p) headOfFree = p -> next;
// егер бос жады болмаса, кезекті блокты бөлім аламыз:
else {
pObj *newblock =static_cast<pObj*>
( : :operator new(BLOCK_SIZE * sizeof(pObj)));
// біріншіден басқа барлық ұяшыұтар бос емес (ол
//бос болмайды). Оларды жинаймыз:
for (int i= 1; i< BLOCK_SIZE — 1; ++i)
newblock[ i ].next = &newblock[ I + 1 ];
newblock[BLOCK_SIZE — 1 ].next = 0;
//бос ұяшықтар орнатамыз:
headOfFree = &newblock[ 1 ];
p =newblock;
}
return p; // Сілтегішті бөлінген жадыға қайтарамыз
}
Қайта анықталған new операторы мұраға беріледі, сондықтан ол туынды обьектілер үшін шақырылады. Егер олардың көлемдері базалық көлемге сай келмесе, бұл ашық жер қалдыруы мүмкін. Оларды болдырмау үшін оператор басында көлемнің сәйкестігі тексеріледі. Егер обьект көлемі new операторы қайта шақырылған көлемге сәйкес келмесе, жадыны бөлуге сұраныс new қалыпты операторына жіберіледі.
p0bj класын қолданатын бағдарламада оның статикалық жолдарының инилизациясы болуы тиіс:
pObj *pObj : :headOfFree; // 0 ге жасырын түрде қойылады
const int pObj: :BLOCK_SIZE = 1024;
Бұл мысалдан көріп отырғанымыздай, жадыны үнемдеуден басқа жоғары тез әсер етуге де қол жеткіземіз. Себебі көп жағдайда жадыны бөлу бірнеше қарапайым операторларға түсіріледі.
Әрине, егер new операторы қайта анықталса, delete операторы үшін де дәл солай болу керек ( мысалы, біздің жағдайымызда delete қалыпты операторы обьект басында оның көлемі туралы дұрыс мәліметті таба алмауы мүмкін, бұл бағдарламаның нақтылы емес әркет етуіне алып келеді).
Void pObj: :operator delete(void * ObjToDie. Size_t size) {
if (ObjToDie == 0) return;
if (size != sizeof(pObj)) {
: :operator delete(ObjToDie); return;
}
pObj *p = static_cast<pObj*>(ObjToDie);
p->next = headOfFree;
headOfFree = p;
}
Delete операторында обьектілер көлемінің сәйкестігінің тексерісі оындалған, ол new операторында келтірілгенге бара-бар.
Бақылау сұрақтары:
- Операторды қайта анықтау дегеніміз не?
- Қайта жүктеу дегеніміз не?
- Қандай операторларды қайта анықтауға болмайды?
- Қайта анықталған операторларды шақыру формалары қандай?
Әдебиеттер:
- Г.С. Иванова и др. ООП: Учебник для вузов, М., Изд-во МГТУ им.Баумана, 2003.- 368 с.
- Павловская Т.А. С и С++. Программирование на языке высокого уровня.
- Р. Лафоре. Объектно-ориентированное программирование в С++.4-е издание. Питер.2004
- Подбельский В.В., Фомин С.С. Программирование на языке Си: Учебное пособие. – М.: Финансы и статистика, 2004