13 skyrius - techmat.vgtu.lt
13 skyrius - techmat.vgtu.lt
13 skyrius - techmat.vgtu.lt
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
<strong>13</strong>. PAVELDIMUMAS<br />
Paveldimumas (angl. inheritance) yra vienas iš objektinio programavimo būdų taupiai rašyti<br />
programos kodą, leidžiantis patogiai kurti naujas klases iš jau turimų klasių, paveldint jų savybes –<br />
laukus ir metodus. Aišku, procedūrinis programavimas irgi turi panašius dalykus – esant reikalui<br />
praplėsti turimos funkcijos galimybes, jos kvietimą galima „apvilkti“ naujos funkcijos kūnu ir<br />
papildomai pridėti naujas galimybes. Vis tik paveldimumo mechanizmas yra taupesnis ir<br />
elegantiškesnis; jis greta vartotojo duomenų formatų (t.y. klasių) ir polimorfizmo (žr. tolesnį skyrių)<br />
laikomas vienu pagrindinių objektinio programavimo principų.<br />
Paveldimumas yra ypač patogus instrumentas taikant klasių bibliotekas: galima naudoti kitų<br />
programuotojų parašytas klases ir jas pritaikyti savo reikmėms ne modifikuojant, o paveldint jų<br />
savybes.<br />
<strong>13</strong>.1 BENDROS ŽINIOS<br />
Klasę, iš kurios paveldėsime visas savybes, vadinsime bazine klase (angl. base class), o ją<br />
naujomis savybėmis plečiančią naująją ar kelias naująsias klases – išvestinėmis klasėmis (angl.<br />
derived class). Tai pavaizduosime principine paveldimumo schema:<br />
Bazinė klasė<br />
Savybė 1<br />
Savybė 2<br />
Savybė3<br />
paveldimumas<br />
Išvestinė klasė<br />
Savybė 1<br />
Savybė 2<br />
Savybė 3<br />
Savybė 4<br />
Sintaksės pavyzdys: anksčiau jau nagrinėta skaitiklio klasė Counter išplečiama išvestine<br />
klase CounterDown, kuri turės vieną papildomą metodą skaitiklio reikšmės mažinimui. Jei, tarkim,<br />
neturėtume prieigos prie Counter klasės (kad tiesiog įdėtume metodą į jos kūną), tai<br />
paveldimumas būtų pats patogiausias kelias Counter funkcionalumui išplėsti.<br />
1 pavyzdys.<br />
162
#include <br />
using namespace std;<br />
//<br />
class Counter{<br />
protected: // 1 pastaba<br />
unsigned int count;<br />
public:<br />
Counter( ): count( 0 ) { } // konstruktorius<br />
Counter( int c ): count( c ) { } // konstruktorius<br />
//<br />
unsigned int returnCount( ){<br />
return count;<br />
}<br />
Counter operator++( ){ // “++” perkrovimas<br />
return Counter( ++count );<br />
}<br />
};<br />
//<br />
class CounterDown: public Counter{ // 2 pastaba<br />
public:<br />
Counter operator--( ){ // “- -” perkrovimas<br />
return Counter( --count );<br />
}<br />
};<br />
//<br />
int main( ){<br />
Counter c1;<br />
CounterDown c2; // 3 pastaba<br />
//<br />
cout
Rezu<strong>lt</strong>atai:<br />
c1: 0<br />
c2: 0<br />
c2: 3<br />
c2: 1<br />
c1: 1<br />
Pastabos:<br />
1. Kad išvestinės klasės metodams būtų prieinami bazinės klasės laukai, būtinas toks<br />
raktažodis. Tačiau jei programoje būtų paveldimumu nesusietų klasių, protected laukai<br />
gretimų klasių metodams – neprieinami. Jei laukai būtų griežtai užverti – private – jie būtų<br />
prieinami tik pačios bazinės klasės viduje; išvestinei klasei – taip pat neprieinami.<br />
2. Taip nurodoma, kad klasė paveldi kitos klasės savybes; čia public reiškia, kad realizuojamas<br />
bendrasis paveldimumas (apie tai skyriaus pabaigoje).<br />
3. Išvestinės klasės konstruktorių nėra – jos objektui formuoti tokiu atveju pasitelkiamas<br />
bazinės klasės atitinkamas konstruktorius (t. y. konstruktorius be argumentų).<br />
4. Išvestinės klasės objektams prieinami bazinės klasės metodai returnCount( ) ir<br />
operator++(), o metodui operator--( ) – ir bazinės klasės konstruktorius su argumentais.<br />
5. Bazinė klasė ją išplėtus nepakinta, todėl jos objektui neprieinami išvestinės klasės<br />
papildomi metodai (čia – operator--( ) ).<br />
Kai programa sudėtinga, patogu pateikti jos klasių struktūrą ir paveldimumo schemą<br />
vadinamąja UML ( angl. Unified Modeling Language, unifikuota modeliavimo kalba) diagrama.<br />
UML diagramoje parodomi klasių pavadinimai, atskiruose laukeliuose išvardinami laukai,<br />
konstruktoriai, destruktoriai ir metodai. Paveldimumas žymimas rodykle link bazinės klasės.<br />
Išvestinės klasės laukeliuose užrašomas tik klasės pavadinimas ir visi papildomi laukai,<br />
konstruktoriai, destruktoriai ir metodai. Jei papildomų savybių laukų ar metodų skyriuose nėra –<br />
šie skyriai paliekami tušti.<br />
Pirmosios programos UML schema:<br />
Counter<br />
count<br />
Bazinė klasė<br />
Counter( )<br />
Counter( int )<br />
returnCounter( )<br />
operator++( )<br />
Išvestinė klasė<br />
CounterDown<br />
operator--( )<br />
164
Dažnai programuotojai projektą sudaro kiek kitaip nei parodyta 1-ajame pavyzdyje: klasių<br />
apibrėžimai sukeliami į antraštinius failus, o programos pradiniame faile lieka tik startinė funkcija.<br />
Aišku, prieš ją direktyvomis kompiliatoriui include turi būti prijungti visi reikiami antraštiniai failai<br />
su klasių apibrėžimais. 1-osios programos projektą galime sukurti taip: 2 pavyzdys:<br />
Antraštinis failas bazinei klasei basicClass.h:<br />
class Counter{<br />
protected:<br />
};<br />
public:<br />
unsigned int count;<br />
Counter( ): count( 0 ) { }<br />
Counter( int c ): count( c ) { }<br />
//<br />
~Counter( ) { }<br />
//<br />
unsigned int returnCount( ){<br />
return count;<br />
}<br />
Counter operator++( ){<br />
return Counter( ++count );<br />
}<br />
Antraštinis failas išvestinei klasei derivedClass.h:<br />
class CounterDown: public Counter{<br />
public:<br />
Counter operator--( ){<br />
return Counter( --count );<br />
}<br />
};<br />
Pradinis programos failas:<br />
#include <br />
#include "basicClass.h"<br />
#include "derivedClass.h"<br />
using namespace std;<br />
//<br />
int main( ){<br />
Counter c1;<br />
CounterDown c2;<br />
165
}<br />
//<br />
cout
}<br />
Counter operator++( ){<br />
return Counter( ++count );<br />
}<br />
};<br />
//<br />
class CounterDown: public Counter{<br />
public:<br />
CounterDown( ): Counter( ) { // 1 pastaba<br />
cout
3. Dabar metodas reikšmę turi grąžinti jau naudodamasis išvestinės klasės konstruktoriumi.<br />
Grąžinti reikšmės klasės Counter konstruktoriumi nevalia, nes kompiliatorius nežinos, kaip<br />
pertvarkyti Counter formato objektą į CounterDown formatą panašiose situacijose, kaip 4-a<br />
pastaba pažymėtame operatoriuje.<br />
4. Tokia sintaksė irgi kviečia išvestinės klasės konstruktorių su argumentu. A<strong>lt</strong>ernatyvi šio<br />
operatoriaus sintaksė būtų CounterDown c3 (--c2 ); . Tokia sintaksė, kai išvestinės klasės<br />
objektai sukonstruojami kopijuojant ir dar atliekant papildomus skirtingose klasėse<br />
apibrėžtus veiksmus (čia – -- arba ++), visad yra kebli. Pabandykit į programą įrašyti<br />
operatorių CounterDown c4 = ++c1; – aišku, bus deklaruota klaida. Turėdami galvoje 3-iąją<br />
pastabą, pabandykit klaidą ištaisyti. Visa tai yra specifiniai sintaksės dalykai, be kurių galima<br />
išsiversti. Jie išsamiai aprašyti <strong>13</strong>.3 skyriuje; pradėdami pažintį su paveldimumu galite šį<br />
skyrių praleisti.<br />
Paleiskite programą ir pamatysite, kad iš tikrųjų prieš konstruojant išvestinės klasės objektą<br />
yra suformuojamas atitinkamas bazinės klasės objektas. Tai yra būtina, kadangi išvestinė klasė<br />
paveldi visus bazinės klasės laukus (ir gali papildyti laukų sąrašą), o jų reikšmėms suformuoti<br />
būtina paleisti bazinės klasės konstruktorių. Galima netgi išreikštai nekviesti bazinės klasės<br />
konstruktorių – jie vis tiek bus iškviesti. Galite tuo įsitikinti taip pakeitę išvestinės klasės<br />
konstruktorius:<br />
CounterDown( ) {<br />
cout
. . .<br />
Atvirkštiniam pertvarkymui būtina parašyti vadinamąjį pertvarkantįjį konstruktorių. Visą<br />
sintaksę paaiškinsim 3-osios programos pagrindu.<br />
4 pavyzdys.<br />
#include <br />
using namespace std;<br />
//<br />
class Counter{<br />
protected:<br />
unsigned int count;<br />
public:<br />
Counter( ): count( 0 ) { }<br />
Counter( int c ): count( c ) { }<br />
//<br />
~Counter( ) { }<br />
//<br />
unsigned int returnCount( ){<br />
return count;<br />
}<br />
Counter operator++( ){<br />
return Counter( ++count );<br />
}<br />
};<br />
//<br />
class CounterDown: public Counter{<br />
public:<br />
CounterDown( ): Counter( ) { }<br />
CounterDown( int c ): Counter( c ) { }<br />
//<br />
~CounterDown( ){ }<br />
//<br />
CounterDown( Counter c ) { // pertvarkantysis konstruktorius<br />
};<br />
//<br />
int main( ){<br />
count = c.returnCount( ); // iš Counter tipo į CounterDown<br />
}<br />
//<br />
CounterDown operator--( ){<br />
return CounterDown( --count );<br />
//return Counter( --count ); // dabar galimas ir toks<br />
// reikšmės grąžinimas: neišreikštai būtų<br />
// kviečiamas pertvarkantysis konstruktorius<br />
}<br />
169
CounterDown c1, c2 = 10;<br />
//<br />
++c2; // gerai: grąžinamas Counter objektas, bet rodyklė<br />
// jį nenutaikyta<br />
c1 = c2; // gerai: abu objektai CounterDown formato<br />
//<br />
CounterDown c3 = --c1; // gerai: metodas "--" grąžina<br />
// CounterDown formato objektą<br />
}<br />
CounterDown c4 = ++c1; // neišreikštai kviečiamas pertvarkantysis<br />
// konstruktorius: "++" grąžina Counter<br />
// objektą; jį reikia pertvarkyti į CounterDown<br />
// formatą<br />
Counter c5 = c1; // neišreikštinis pertvarkymas iš CounterDown į bazinį<br />
// Counter formatą: visada leidžiamas<br />
cout
}<br />
Counter operator++( ){<br />
return Counter( ++count );<br />
}<br />
};<br />
//<br />
class CounterDown: public Counter{<br />
public:<br />
CounterDown( ): Counter( ) { }<br />
CounterDown( int c ): Counter( c ) { }<br />
//<br />
~CounterDown( ){ }<br />
//<br />
explicit CounterDown( Counter c ) { // išreikštinis<br />
count = c.returnCount( ); // pertvarkantysis konstruktorius<br />
}<br />
//<br />
CounterDown operator--( ){<br />
return CounterDown( --count );<br />
//return Counter( --count ); // dabar nebegalimas<br />
}<br />
};<br />
//<br />
int main( ){<br />
CounterDown c1, c2 = 10;<br />
//<br />
++c2;<br />
//<br />
c1 = c2;<br />
//<br />
CounterDown c3 = --c1;<br />
//<br />
//CounterDown c4 = ++c1; // dabar nebegalimas<br />
CounterDown c4 = static_cast( ++c1 ); // išreikštinis<br />
// tipų pertvarkymas;<br />
// rekomenduojamas stilius<br />
//CounterDown c4 = (CounterDown)( ++c1 ); // a<strong>lt</strong>ernatyvi sintaksė<br />
//<br />
Counter c5 = static_cast( c1 ); // išreikštinis pertvarkymas<br />
//Counter c5 = (Counter)c1;<br />
//<br />
cout
}<br />
system( "pause" );<br />
return 0;<br />
<strong>13</strong>.4 PAVELDIMUMAS IR METODŲ PERKROVIMAS<br />
Nagrinėsime bazinės klasės metodų perrašymą išvestinėje klasėje. Dabar išvestinės klasės<br />
metodų vardai sutampa su bazinės klasės metodų vardais. Kurie metodai bus kviečiami bazinės ir<br />
išvestinės klasių objektams?<br />
6 pavyzdys: steko iš trijų elementų klasė. Stekas – paprasta duomenų struktūra, nedidelio<br />
mato vienmatis masyvas laikinam duomenų saugojimui. Priklausomai nuo to, kokia tvarka į steką<br />
įdedami ir iš jo išimami duomenys, stekai būna įvairių rūšių: FIFO (angl. First In, First Out – pirmas<br />
įėjo, pirmas išėjo), FILO, LIFO ir pan. Čia parašysime steką trims sveikiesiems duomenims saugoti<br />
„paskutinis įėjo, pirmas išėjo“ (LIFO). Tegu turim nesaugią klasę Stack, kuri netikrina, ar stekas<br />
neperpildytas ir ar nėra tuščias. Stekas turi turėti bent du metodus: push duomeniui į steką<br />
(vienamtį masyvą stack) įdėti ir pop – duomeniui iš steko išimti ir grąžinti. Į masyvą įdedamo ir<br />
išimamo elemento vietą masyve žymėsime indeksu top. Klasės funkcionalumą paveldėsime ir<br />
papildysime duomenų įdėjimo ir išėmimo metodais su reikiamomis patikromis – tai bus jau<br />
saugaus steko klasė SafeStack. Masyvo matas Stack klasėje užduodamas konstrukcija enum: tik<br />
taip galima „apgauti“ kompiliatorių ir klasės viduje pranešti konkrečią duomens reikšmę 3.<br />
#include <br />
#include <br />
using namespace std;<br />
//<br />
class Stack{<br />
protected:<br />
enum{ MAX = 3 };<br />
int stack[ MAX ];<br />
int top;<br />
public:<br />
Stack( ): top(-1) { }<br />
//<br />
~Stack( ) { }<br />
//<br />
void push( int var ) { stack[ ++top ] = var; }<br />
int pop( ) { return stack[ top-- ]; }<br />
};<br />
//<br />
class SafeStack: public Stack {<br />
public:<br />
void push( int var ){<br />
if( top >= MAX-1 ){<br />
cout
}<br />
int pop( ){<br />
}<br />
}<br />
if( top < 0 ){<br />
}<br />
system( "pause" );<br />
exit( 1 );<br />
Stack::push( var ); // pastaba<br />
cout
Iš rezu<strong>lt</strong>atų aiškiai matyti, kad SafeStack formato objektui s kviečiami klasėje SafeStack<br />
perrašyti metodai. Paskelbkite objektą s Stack formato ir pamatysite, kad programa spausdins 4 iš<br />
steko grąžinamus skaičius, kurių paskutinis bus neapibrėžtos reikšmės – t.y. bus kviečiami<br />
nesaugūs Stack klasės atitinkami metodai.<br />
<strong>13</strong>.5 BENDRASIS IR DALINIS PAVELDIMUMAS<br />
Pirmą kartą skaitydami paveldimumo skyrių, galite šį skyrelį praleisti.<br />
Visose programose anksčiau rodydami, kad išvestinė klasė paveldi savybes iš bazinės,<br />
taikėme bendrąjį paveldimumą: class SafeStack: public Stack { . . . Vietoje raktažodžio public<br />
galima rašyti ir private: tokiu atveju turėsime vadinamąjį dalinį paveldėjimą. Šie terminai nurodo,<br />
kokią prieigą prie bazinės klasės laukų turi išvestinių klasių objektai. Visi galimi prieigos lygiai<br />
parodyti 7 pavyzdžio abstrakčioje, jokios prasmės neturinčioje programoje:<br />
#include <br />
using namespace std;<br />
//<br />
class A{<br />
private:<br />
int privateData;<br />
protected:<br />
int protectedData;<br />
public:<br />
int publicData;<br />
//<br />
A( ): privateData( 0 ), protectedData( 0 ), publicData( 0 ){ };<br />
};<br />
//<br />
class B: public A{ // bendrasis paveldimumas<br />
public:<br />
void m( ){<br />
int data;<br />
//data = privateData; // klaida<br />
data = protectedData;<br />
data = publicData;<br />
};<br />
//<br />
}<br />
class C: private A{ // dalinis paveldimumas<br />
public:<br />
void m( ){<br />
int data;<br />
//data = privateData;<br />
// klaida<br />
174
}<br />
data = protectedData;<br />
data = publicData;<br />
};<br />
//<br />
int main( ){<br />
int data;<br />
A a;<br />
B b;<br />
C c;<br />
//<br />
//data = a.privateData; // klaida<br />
//data = a.protectedData; // klaida<br />
data = a.publicData;<br />
//<br />
//data = b.privateData; // klaida<br />
//data = b.protectedData; //klaida<br />
data = b.publicData;<br />
//<br />
}<br />
//<br />
system( "pause" );<br />
return 0;<br />
//data = c.privateData; // klaida<br />
//data = c.protectedData; //klaida<br />
//data = c.publicData; // klaida<br />
Kaip matyti, nepaisant paveldimumo tipų, išvestinių klasių metodai vienodai „nemato“<br />
uždarų bazinės klasės laukų, o apsaugotus ir viešus laukus – „mato“. Su klasių objektais – kitokia<br />
istorija: private ir protected laukai visų klasių objektams yra neprieinami. Vienintelis abiejų<br />
paveldimumo tipų skirtumas – kad esant daliniam paveldimumui išvestinės klasės objektams<br />
neprieinami ir bazinės klasės public laukai (o taip pat ir metodai, tik paskutinis dalykas schemoje<br />
nėra iliustruotas).<br />
Dalinį paveldėjimą reikėtų taikyti, norint nuo išvestinės klasės objektų paslėpti visus<br />
bazinių klasių laukus ir metodus – tai būtų aktualu, pavyzdžiui, SafeStack klasės atveju, norint jokiu<br />
būdu neleisti taikyti nesaugių atvirų klasės Stack metodų.<br />
<strong>13</strong>.6 DAUGYBINIS PAVELDIMUMAS<br />
Idėja yra akivaizdi: išvestinė klasė paveldi iš karto kelių klasių savybes. Sintaksiškai tai ne<br />
visada saugu, nes kartais kelių klasių funkcionalumas gali tarpusavyje nederėti. Pavyzdys UML<br />
diagramos pavidalu:<br />
175
A<br />
B<br />
C<br />
Sintaksė:<br />
class A{ };<br />
class B{ };<br />
class C: public A, public B { };<br />
Jei nėra bazinių klasių nesuderinamumų, tai daugybinis paveldimumas nuo vienatinio<br />
skiriasi faktiškai tik konstruktorių naudojimu.<br />
8 pavyzdys. Rašoma klasė Lumber informacijai apie medienos gaminius talpinti. Tarkim, ji<br />
gali dalį funkcionalumo paveldėti iš jau parašytos klasės Distance (int ir double formato laukus -<br />
gaminio ilgį pėdomis ir coliais) ir dalį – iš klasės Type (string formato – skerspjūvio duomenis ir<br />
rūšį), o šiuos laukus dar papildo int formato duomeniu kiekiui ir double – kainai talpinti.<br />
#include <br />
#include <br />
using namespace std;<br />
//<br />
class Type{<br />
protected:<br />
string dimensions;<br />
string grade;<br />
public:<br />
Type( ): dimensions( "N/A" ), grade( "N/A" ) { }<br />
Type( string d, string g ): dimensions( d ), grade( g ) { }<br />
//<br />
~Type( ) { }<br />
//<br />
void getType( ){<br />
coutdimensions>>grade;<br />
}<br />
void showType( ){<br />
cout
};<br />
//<br />
class Distance{<br />
private:<br />
public:<br />
}<br />
int feet;<br />
double inches;<br />
Distance( ): feet( 0 ), inches( 0. ) { }<br />
Distance( int ft, double in): feet( ft ), inches( in ) { }<br />
//<br />
~Distance( ) { }<br />
//<br />
void getDistance( ){<br />
coutfeet>>inches;<br />
}<br />
void showDistance( ){<br />
cout
int main( ){<br />
Lumber p1;<br />
Lumber p2( "2x4", "A", 8, 0., 100, 50.00 );<br />
//<br />
p1.getLumber( );<br />
p1.showLumber( );<br />
p2.showLumber( );<br />
//<br />
system( "pause" );<br />
return 0;<br />
}<br />
Nors programos tekstas ilgas, logiškai ji paprastutė. Ši programa neatlieka jokių veiksmų, tik<br />
įveda dviejų Lumber klasės objektų duomenis ir juos parodo. Komentarų ženklu 1 pažymėti<br />
operatoriai parodo, kaip kviečiami abiejų bazinių klasių konstruktoriai, o vėliau užpildomi<br />
išvestinės klasės papildomi laukai. Operatoriai su komentarais 2 rodo, kad bazinės klasės<br />
metodams kviesti būtina priklausomybės operacija :: .<br />
Minėjome, kad daugybinis paveldimumas ne visada saugus. Matyt, todėl vėlesnėje C++<br />
pagrindu sukurtoje kalboje Java daugybinio paveldimumo iš viso nebeliko; panašius efektus ten<br />
pasiekti galima tik sudėtingomis programinėmis schemomis. Čia parodysime tik kelis galimus<br />
daugybinio paveldimumo neapibrėžtumus.<br />
9 pavyzdys. Bazinių klasių funkcionalumas kartojasi (čia – vienodi metodai):<br />
#include <br />
using namespace std;<br />
//<br />
class A{<br />
public:<br />
void m( ){ }<br />
};<br />
//<br />
class B{<br />
public:<br />
void m( ){ }<br />
};<br />
//<br />
class C: public A, public B{ };<br />
//<br />
int main( ){<br />
C c;<br />
//c.m( ); // klaida! Neaišku, kurį metodą kviesti<br />
c.A::m( ); // gerai: kviečiamas iš A paveldėtas metodas<br />
c.B::m( ); // gerai: kviečiamas iš B paveldėtas metodas<br />
//<br />
system( "pause" );<br />
return 0;<br />
178
}<br />
10 pavyzdys. Trijų lygių paveldimumas. Dabar klasė B paveldi visas klasių A1 ir A2 savybes –<br />
taigi ir metodą m, kurį šios savo ruožtu nukopijavo iš klasės A. Nors metodas visiškai vienodas<br />
visose trijose klasėse A, A1 ir A2, klasėje B tampa nebeaišku, kurią konkrečiai iš metodo kopijų<br />
reikia kviesti:<br />
#include <br />
using namespace std;<br />
//<br />
class A{<br />
public:<br />
void m( ){<br />
cout