Home Информационни технологии ПРОИЗВОДНИ КЛАСОВЕ, НАСЛЕДЯВАНЕ И ПОЛИМОРФИЗЪМ

***ДОСТЪП ДО САЙТА***

ДО МОМЕНТА НИ ПОСЕТИХА НАД 2 500 000 ПОТРЕБИТЕЛИ

БЕЗПЛАТНИТЕ УЧЕБНИ МАТЕРИАЛИ ПРИ НАС СА НАД 7 700


Ако сме Ви били полезни, моля да изпратите SMS с текст STG на номер 1092. Цената на SMS е 2,40 лв. с ДДС.

Вашият СМС ще допринесе за обогатяване съдържанието на сайта.

SMS Login

За да използвате ПЪЛНОТО съдържание на сайта изпратете SMS с текст STG на номер 1092 (обща стойност 2.40лв.)


SMS e валиден 1 час
ПРОИЗВОДНИ КЛАСОВЕ, НАСЛЕДЯВАНЕ И ПОЛИМОРФИЗЪМ ПДФ Печат Е-мейл

7. ПРОИЗВОДНИ КЛАСОВЕ, НАСЛЕДЯВАНЕ И ПОЛИМОРФИЗЪМ

7.1. ДЕКЛАРИРАНЕ НА КЛАС КАТО ПРОИЗВОДЕН

Всеки клас може да дъде деклариран като производен клас (клас наследник)  на други вече декларирани класове, които, с това, стават негови базови класове. Понятията базов и производен клас са относителни, тъй като всеки производен клас може да бъде базов за други класовe.

Даден клас D се декларира като производен на класa B както следва:

class D : [public|private|protected] B {

};

Пред иметo на базовия клас може да се постави една от ключовите думи public, private или protected (private e подразбираща се, а protected тук е равнозначна на private). Тяхното значение ще бъде изяснено по-надолу.

С декларирането на даден клас като производен, той ще получи достъп до компонентите на базовия клас, регламентиран от правила, които са разгледани по-нататък.

Програма 7.1. е примерна програма за дефиниране и използване на производен клас. В нея е дефиниран един производен клас с име TheCar, който има един базов клас с име Car.

Програма 7.1. Базов клас Car и производен клас TheCar

#include

#include

#include

//  Декларация на клас Car, който е базов за класа TheCar

class Car {

public:

void makeCar(char *,int);

void printCar();

private:

char *mark;       //Марка

int year;                             //Година на производство

};

//  Дефиниция на метод makeCar на класа Car

void Car::makeCar(char *a,int b)

{

mark=new char[strlen(a)+1];

strcpy(mark,a);

year=b;

}

//  Дефиниция на метод printCar на класа Car

void Car::printCar()

{

cout<<"Марка: "<

cout<<"Година: "<

}

// Декларация на производен клас TheCar с базов клас Car

class TheCar:Car{

public:

void makeTheCar(char *,int,int);

void printTheCar();

private:

int reg_numb;                //Регистрационен номер

};

// Дефиниция на метод makeTheCar класа Car

void TheCar::makeTheCar(char *a,int b,int c)

{

makeCar(a,b);

reg_numb=c;

}

// Дефиниция на метод printTheCar класа Car

void TheCar::printTheCar()

{

printCar();

cout<<"Регистрационен номер: "<

}

//Дефиниция на функцията main

void main()

{

TheCar mycar; //Създаване на обект mycar от класа TheCar

mycar.makeTheCar("FIAT",1992,2568);                     // Инициализиране

mycar.printTheCar();                      //Извеждане на стойностите

getch();

}

Класът Car представя понятието “автомобил”. За да не се усложнява примера е прието, че автомобилът се представя само със своята марка и година на производство. Тези характеристики се представят от данните на класа char *mark и int year. Освен данни, класът Car съдържа и методите makeCar и printCar. Чрез метода makeCar се инициализират обектите на класа Car, a методът printCar() извежда на екрана стойностите на променливите mark и year.

Класът TheCar представя понятието "конкретен автомобил", защото съдържа и характеристиката “регистрационен номер". Той е деклариран като производен на класа Car и следователно наследява характеристиките на понятието "автомобил". В резултат на това класът TheCar ще има седем компоненти. Четири от тях (промен­ливите mark и year и методите makeCar и printCar) се наследяват от базовия клас Car и още три (int reg_numb, makeTheCar, printTheCar) са на самия производен клас TheCar.

7.2. ДОСТЪП ДО НАСЛЕДЕНИТЕ КОМПОНЕНТИ

Нека да си припомним, че при деклариране на компонентите на даден клас посочваме и тяхната достъпност за външни функции - public компонентите са достъпни за външните функции, а protected и private компонентите не са достъпни за външните функции. Може да се каже, че с тези ключови думи се филтрира достъпа на външните функции до компонентите на класовете.

При производен клас съществуват две нива на достъп:

- достъп на методите на производния клас до компонентите на базовия клас - независимо как е деклариран базовият клас в производния, за методите на производния клас са достъпни само онези от компонентите на базовия клас, които в базовия клас са декларирани като public или protected;

-          достъп на външни функции до наследените компоненти на обекти от производния клас, например достъп на функцията main до компонентата mycar.аge, която обектът mycar от проиаводния клас е наследил от базовия клас Car – такъв достъп съществува само за public компонентите на базов клас, който е деклариран като public в производния клас.

Методите на производните класове нямат достъп до наследените компоненти, които са декларирани като private в базовите класове. Например в програма 7.1 наследените от базовия клас компоненти mark и year са недостъпни за метода makeTheCar на производния клас TheCar, тъй като са декларирани като private в базовия клас. Поради това компонентите mark и уеаг се инициализират чрез извикване на метода makeCar(a,b), който също е наследен, но е достъпен за метода makeTheCar, тъй като е деклариран като public в базовия клас Car. Същият подход е използуван и в метода printTheCar на класа TheCar. Той използува метода printCar на класа Car, чрез който се извеждат на екрана стойностите на променливите mark и year.

В редица случаи е желателно, а понякога е наложително, наследените компоненти на един производен клас да бъдат достъпни за методите на този клас, но не и за външни функции. Такъв режим на достъп се задава чрез деклариране на компонентите в базовите класове като protected (виж табл. 7.1 и 7.2) Програма 7.2 е вариант на програма 7.1, чрез която се илюстрира действието на ключовата дума protected.

Програма 7.2.Програма с базов клас Car u производен клас TheCar. Използуване на ключовата дума protected

#include

#include

#include

//Декларация на клас Car

class Car {

public:

void makeCar (char *, int );

void printCar();

protected:

//protected компонентите ще бъдат видими за всички производни

//класове на класа Car, но не и за външни функции

char *mark;                     //Mapкa

int year;                                           //Година на производство

};

//Дефиниция на метод makeCar на клaca Car

void Car::makeCar(char *a, int b)

{

mark = new char[ strlen(a) + 1 ];

strcpy ( mark, a );

year = b;

}

//Дефиниция на метод printCar на клaca Car

void Car::printCar()

{

cout <<"Mapкa " <

cout <<"Година " <

}

//Декларация на производен клac TheCar с базов клас Car

class TheCar : public Car {

public:

void makeTheCar ( char *, int, int );

void printTheCar();

protected:

int  reg_numb;                // Регистрационен No

};

//Дефиниция на метод makeTheCar

void TheCar:: makeTheCar ( char *a, int b, int c )

{

mark = new char[strlen(a)+1]; //Заделяне на динамична памет

strcpy ( mark, a );

year = b;

reg_numb = c;

}

//Дефиниция на метод printTheCar

void TheCar::printTheCar()

{

cout <<"Mapкa " <

cout <<"Година " <

cout <<"Регистрационен No " <

}

//Дефиниция на функия main

main()

{

TheCar mycar;   //Създаване на o6eкm mycar om клac TheCar

mycar.makeTheCar ("FIAT", 2001, 2222); //Извиква се конcтpyктop

mycar.printTheCar();        //Извеждане на данните

mycar.reg_numb =2345; // !!! ГРЕШКА. reg_numb e недостъпен за външни

//функии, кaквaтo e main()

getch();

}

Методите на производния клас TheCar, дефиниран в тази програма, имат достъп до променливите mark и year на своя базов клас Car, тъй като те са декларирани като protected. Поради това методите makeTheCar и printTheCar не използват методите makeCar и printCar на класа Car като средства за достъп до неговите данни, както е в програма 7.1. Но функцията main, която е външна за класа TheCar няма достъп до неговите protected компоненти, какъвто е променливата reg_numb. Поради това компилаторът ще регистрира грешка в последния ред на функцията main.

Използуването на производни класове има следните предимства:

- Дефинирането на производни класове e еквивалентно на конструиране на йерархии от класове, които са адекватен модел на йерархиите между понятията от реалния свят.

- Ако множество класове имат общи части (данни и методи), тези общи части могат да се обособят като базови класове (образно казано да се извадят пред скоби), а всеки от множеството класове да бъде дефиниран като производен на съответните базови класове. По този начин се избягва многократното описание на едни и същи фрагменти, като в някои случаи това води и до икономия на памет.

- При създаването на производни класове е достатъчно да се разполага само с обектните модули на базовите класове, а не с техния програмен текст. Това позволява предварително да бъдат създавани библиотеки от класове, които в последствие да бъдат използувани като базови при създаването на различни производни класове за конкретни нужди.

- Производните класове, съчетани с използуването на виртуални методи (виртуалните методи се разглеждат в настоящата глава), са средство за реализиране на полиморфизъм в езика C++. Полиморфизмът дава възможност за еднотипна обработка на информационни структури като списъци, масиви, дървета, графи и др., чиито елементи са обекти от различни класове.

7.3. КОНСТРУКТОРИ И ДЕСТРУКТОРИ НА ПРОИЗВОДНИ КЛАСОВЕ

Производните класове могат да имат конструктори, но изпълнението на конструкторите и деструкторите на производните класове протича в съответствие със следните правила:

- Извикването на конструктор на производен клас води до извикване на конструкторите на неговите базови класове и след завършване на тяхното изпълнение се изпълнява конструкторът на производния клас.

- Деструкторите на производния клас и на неговите базови класове се изпълняват в ред, обратен на реда на изпълнение на техните конструктори. Най-напред се изпълнява деструкторът на производния клас, а след това - деструкторите на неговите базови класове.

При използуването на конструктори и деструктори на производни и базови класове трябва внимателно да се анализира последо­вателността на тяхното изпълнение, определена от горните правила, тъй като лесно могат да бъдат допуснати грешки. Представете си например, че една динамична променлива, която е дефинирана като protected в базовия клас Base се използува и от производния клас Derived. Лесно може да се допусне грешка като се направи опит два пъти да се освободи паметта за тази променлина - веднъж в деструктора на Derived и веднъж в деструктора на Base.

7.3.1. Конструктори с параметри

Използуването на конструктори на производни класове, чиито базови класове имат конструктори с параметри, е свързано с някои особености. Нека е дефиниран един производен клас Derived, който има два базови класа В1 и В2. Ако конструкторите на базовите класове имат по един параметър от тип int и float съответно, то тези параметри трябва да бъдат включени в списъка от параметри на конструктора на производния клас, за да могат да се предават на конструкторите на базовите класове. За да се реализира предаването на параметрите на конструкторите на базовите класове, конструкторът на производния клас Derived трябва да бъде дефиниран по следния начин:

Derived :: Derived ( int х, float y, int z ) : B1(x), B2(y)

{

. . . . . . . . . . . . . . . . . . .

}

Първите два от параметрите на конструктора на класа Derived са необходими, само за да бъдат предадени на конструкторите на базовите класове. Параметрите се предават чрез явното извикване на конструкторите на базовите класове. Забележете, че извикването им не е в тялото на конструктора на производния клас Derived, а непосредствено след затварящата скоба на списъка с параметрите му. Причината за това е обстоятелството, че конструкторите на базовите класове се изпълняват след извикването, но преди изпълнението на конструктора на производния клас.

Използуването на конструктори и деструктори на производни класове е илюстрирано в програма 7.3. Тя се различава от програма 7.1 пo това, че методите makeCar и makeTheCar от програма 7.1 са заменени с конструктори на класовете Car и TheCar. Освен това в класа Car на програма 7.3 е добавен деструктор, който освобождава паметта на динамичната променлива mark.

Програма 7.3.Кoнcтpyктopu на производни и базови класове с параметри

#include

#include

#include

//Декларация на клас Car

class Car{                                             //Базов клас Car

public:

Car(char *,int);                // Кoнcтpyктop

void printCar();

~Car() {delete mark;}      // Дecтpyктop

private:

char *mark;

int year;

};

//Дефиниция на кoнcтpyктopa

Car::Car(char *a,int b)

{

mark=new char[strlen(a)+1];           // Заделяне на динам. памет

strcpy(mark,a);

year=b;

}

//Дефиниция на метода printCar

void Car::printCar()

{

cout<<"Mapкa "<

cout<<"Година "<

}

//Декарация на производния клac TheCar

class TheCar:public Car {

public:

TheCar(char *,int,int); //Кoнcтpyктop

void printTheCar();

private:

int reg_numb;

};

//Дефиниция на кoнcтpyктopa на производния клac TheCar

TheCar::TheCar(char *a,int b,int c):Car(a,b)

{

reg_numb=c;

}

//Дефиниция на метода printTheCar

void TheCar::printTheCar()

{

printCar();

cout<<"Peгиcтp. No "<

}

//Дефиниция на фуниция main

main()

{

TheCar mycar("Ореl",1989,2367); //Параметри на кoнcтpуктора

mycar.printTheCar();

getch();

}

7.4. ЕДНОИМЕННИ МЕТОДИ В ПРОИЗВОДНИ И БАЗОВИ КЛАСОВЕ

Много често при проектирането на различни класове се предвиждат еднакви по смисъл операции, прилагащи се върху различни по тип данни. От методологическа гледна точка е целесъобразно методите на различните класове, реализиращи подобни операции, да бъдат с еднакви имена. Поради локалността на методите в рамките на класовете, използуването на еднакви имена на методите в различни класове не води до конфликт.

По-особен е случаят, когато един производен клас и някой негов базов клас съдържат методи с еднакви имена. Нека методът f() е дефиниран в два класа А и В, като класът В е базов за класа А. Тогава класът А ще притежава два метода с име f(). Единият от тях е методът на класа А, а другият е наследеният от базовия клас В. Извикването на метода f() чрез обект на класа А (например Оbj.f(), където Оbj е обект на класа А ) винаги ще води до изпълнение на метода f() на класа А. За да се извика наследеният метод f(), трябва да се използува неговото пълно име, състоящо се от името на класа, на който той принадлежи (в случая това е клaca В), оператора за принадлежност :: и името на метода. Следователно извикването на наследения метод f() се задава чрез конструкцията B::f(). Използуването на едноименни методи в производни и базови класове е илюстрирано в програма 7.4.

Програма 7.4.  Едноименен метод в базов и производен клас

#include

#include

#include

//Дефиниция на базов клас Base_class

class Base_class {

public:

void getData()

{

coutx;

}

protected:

int x;

};

//Декларация на производен клас Derived

class Derived:public Base_class {

public:

void getData()                                 //Дефиниция на метода getData

{

Base_class::getData();//Извикване на метода get_data() на базовия клас

couty;

}

protected:

int y;

};

//Дефиниция на фунрция main

main()

{

Derived c1;

c1.getData();                                      //Извикване на Derived::getData

c1.Base_class::getData();                //Наследеният метод get_data()

getch();

}

7.5. МНОЖЕСТВЕНО НАСЛЕДЯВАНЕ

Когато един производен клас има няколко базови класа, той наследява компонентите на всички базови класове. Това се нарича множествено наследяване.

Даден клас D се декларира като производен на класовете B1, B2, . . . , Bn както следва:

class D : [public|private|protected] B1,...,[public|private] Bn{

};

Пред всяко от имената на базовите класове може да се постави една от ключовите думи public, private или protected. Достъпът до наследените компоненти се подчинява на правилата, разгледани в т.7.2 на настоящата глава.

В примерната програма 7.5 се използва производен клас, който има два базови класа.

Програма 7.5. Програма с производен клас с два базови класа

#include

#include

#include

//Декларация на клас Car

class Car {

public:

Car(char *,int);                          // Конструктор

void printCar();

~Car(){delete mark;}       // Деструктор

private:

char *mark;

int year;

};

//Дефиниция на кoнcтpyктop на класа Car

Car::Car(char *a,int b)

{

mark=new char[strlen(a)+1];

strcpy(mark,a);

year=b;

}

//Дефиниция на метод printCar

void Car::printCar()

{

cout<<"Mapкa: "<

cout<<"Година: "<

}

//Декарация на производен клac TheCar с един базов клac

class TheCar:public Car {

public:

TheCar(char *,int,int); //Кoнcтpyктop

void printTheCar();

private:

int reg_numb;

};

//Дефиниция на кoнcтpyктopa на клaca TheCar

TheCar::TheCar(char *a,int b,int c):Car(a,b)

{

reg_numb=c;

}

//Дефиниция на метод printTheCar

void TheCar::printTheCar()

{

printCar();

cout<<"Peaucтp. No: "<

}

//Декларация на клac Person

class Person {

public:

Person(char *,int );                         // Кoнcтpyктop

~Person () {delete name;}             // Деструктор

void display();

protected:

char *name;

int age;

};

//Дефиниция на кoнстpyктopa на клaca Person

Person::Person(char *a,int b)

{

char[strlen(a)+1];          //Заделяне на динамична памет

strcpy(name,a);

age=b;

}

//Дефиниция на метод display

void Person::display()

{

cout<<"Име: "<

cout<<"Възраст: "<

}

//Декларация на производен клac с два базови клaca

class CarPassport:public TheCar,public Person {

public:

CarPassport(char *,int,int,char *,int);

void display();

};

//Дефиниция на кoнcтpyктop на клaca CarPassport

CarPassport::CarPassport(char *mark,int year,int reg_numb,

char *name,int age):TheCar(mark,year,reg_numb),Person(name,age)

{

//Празно тяло

}

//Дефиниция на метод display на клac CarPassport

void CarPassport::display()

{

printTheCar();

Person::display();

}

//Дефиниция на функция main

main()

{

CarPassport x("FORD",1990,4567,"ГЕОРГИ",33);

x.display();

getch();

}

В тази програма е дефиниран един клас с име CarPassport, съответствуващ на понятието "паспорт на автомобил". Паспортът на автомобила е документ, съдържащ данните за даден автомобил и данните за неговия собственик. Тъй като тези данни са определени чрез класовете TheCar и Person, класът CarPassport е дефиниран като производен на тези два класа и наследява от тях необходимите компоненти (данни и методи). Обърнете внимание на конструктора на производния клас CarPassport. Тялото на този конструктор е празно, тъй като единственото му предназначение е да извика конструкторите на базовите класове TheCar и Person със съответните параметри.

7.6. ДОСТЪП ДО НАСЛЕДЕНИ КОМПОНЕНТИ, КОИТО ИМАТ ЕДНАКВИ ИМЕНА

Поради множественото наследяване, в производни класове, които имат няколко базови класа, може да възникне ситуация, която няма аналог при използуване на производни класове, имащи само един базов клас. Например, ако в различни базови класове на един производен клас бъдат дефинирани компоненти с еднакви имена, то в резултат на множественото наследяване всички те ще бъдат наследени от производния клас. За да бъдат различавани тези компоненти в рамките на производния клас, те се използват с пълните им имена, както е показано в програма 7.6.

Програма 7.6. Наследени компоненти с еднакви имена

#include

#include

//Дефиниция на клас Base1

class Base1 {

public:

Base1() {coutx;} //Кoнcтpyктop

protected:

int x;

};

//Дефиниция на клас Base2

class Base2 {

public:

Base2(){coutx;}     //Кoнстpyктop

protected:

int x;

};

//Дефиниция на производен клас Derived

class Derived:public Base1,public Base2 {

public:

void storeData();

void printData();

protected:

int z1,z2;

};

//Дефиниция на метод storeDdata

void Derived::storeData()

{

z1=Base1::x;                       //Достъп до х, наследена от класа Base1

z2=Base2::x;                       //Достъп до х, наследена от класа Base2

}

//Дефиниция на метод printDdata

void Derived::printData()

{

cout<<"z1="<

cout<<"z2="<

}

//Дефиниция на функция main

main()

{

Derived d;           //Създаване на oбeкт d на произв. клас Derived

d.storeData();

d.printData();

getch();

}

Класът Derived наследява две променливи с име х - едната от базовия клас Base1, а другата от базовия клас Base2. В метода storeData() на производния клас Derived те са достъпътни чрез пълните им имена Base1::x и Base2::x.

7.7. ВИРТУАЛНИ КЛАСОВЕ

В резултат на множественото наследяване могат да възникват ситуации, при които някои производни класове наследяват многократно едни и същи компоненти. Ще поясним това с помощта на един пример. Нека са дефинирани класовете С, B1, B2 и D така, че класовете B1 и B2 са производни на класа С и базови за класа D (фиг. 7.1).

Фиг. 7.1.

Като производни на класа С, двата класа B1 и B2 ще наследят неговите компоненти. Но от друга страна класовете B1 и B2 са базови за класа D. Следователно класът D ще наследи два пъти компонентите на класа С - веднъж от B1 и веднъж от B2.

Многократното наследяване на едни и същи компоненти може да бъде предотвратено чрез използуване на виртуални класове. Например, ако класът D трябва да наследи само веднъж компонентите на класа С, то в декларациите на класовете B1 и B2 класът С трябва да бъде обявен като виртуален. Това става чрез поставяне на ключовата дума virtual пред името на класа С в декларациите на класовете B1 и B2, т.е. класовете С, B1, B2 и D трябва да бъдат декларирани по следния начин:

class С {

............

};

class Bl : public virtual С { //Виртуален базов клас С

............

};

class B2 : virtual public С { //Виртуален базов клас С

............

};

class D : public Bl, public B2 {

............

};

Редът, в който се записват думите public (или private) и virtual пред имената на базовите класове в декларациите на производните класове, е без значение.

Някои версии на езика С++, включително BORLAND C++, имат една особеност, отнасяща се до виртуалните класове, които имат конструктори с параметри. Тази особеност се заключава в следното:

Нека класът Х е виртуален базов клас за класа Y, а класът Y е базов за класа Z. Ако класът Х има конструктор с параметри, този конструктор трябва да бъде извикван не само от конструктора на класа Y, но и от конструктора на класа Z. Ако класът Z има наследници (класове, за които Z е базов), то конструкторът на виртуалния клас Х трябва да се извиква и от конструкторите на наследниците на Z и т.н. Общото правило за извикване на конструктори с параметри на виртуални класове може да бъде формулирано така: конструкторите на виртуалните класове трябва да се извикват от конструкторите на всички класове, които са техни наследници, а не само от конструкторите на преките наследници. Това правило е илюстрирано в програма 7.7, в която са дефинирани класовете С, B1, B2 и D съгласно схемата, показана на фиг. 7.1, като всеки от тях има конструктор с параметри.

Програма 7.7. Виртуални класове, кoитo имат кocтpyктopи с параметри

#include

#include

//Дефиниция на клac С

class C {

public:

int x;

C(int a) {x=a;}                                  //Кoнcтpyктop с параметри

void displayC()

{cout<<"Печат - С х = "<

};

//Дeклapaцuя на клac B1

class B1:virtual public C {                 //Виртуален базов клac

public:

B1(int);                                                             //Кoнcтpyктop

void displayB1()

{cout<<"Печат - B1 x = "<

};

//Дефиниция на кoнcтpyктopa на клaca B1

B1::B1(int a):C(a)

{

}

//Декларация на клac B2

class B2 :  virtual public C {              //Виртуален базов клac

public:

B2( int );                            //Кoнcтpyктop

void displayB2()

{cout<<"печат - B2 x = "<

};

//Дефиниция на кoнcтpyктopa на клaca B2

B2::B2(int a):C(a)

{}

//Декарация на клac D

class D:public B1,public B2 {           //Два базови клaca B1 и В2

public:

D(int);                                               //Кoнcтpyктop

void displayD() {cout<<"Печат - D x= "<

};

//Дефиниция на кoнcтpyктopa на клaca D

//!!!! Извиквa се явно кoнстpyктopът на клaca C, въnpeкu че D не е npяк

//нacлeднuк на клaca C

D::D(int a):C(a),B1(a),B2(a)

{}

//Дефиниция на функция main

main()

{

D obj(2);

obj.displayC(); // Методът е наследен само веднъж от класа С

obj.displayB1();

obj.displayB2();

obj.displayD();

getch();

}

Обърнете внимание на дефиницията на конструктора на класа D:

D::D(intа):С(а),В1(а),В2(а){}

Този конструктор изникна не само конструкторите на своите базови класове B1 и B2, но и конструктора на класа С, въпреки че класът D не е пряк наследник на класа С. Ако класът С не беше виртуален, неговият конструктор нямате да се извиква от конструктора на класа D, а само от конструкторите на класовете B1 и B2.

7.8. ВИРТУАЛНИ МЕТОДИ (ВИРТУАЛНИ ФУНКЦИИ)

В един производен клас и в негови базови класове могат да се дефинират методи с еднакви прототипи (име, параметри и тип на върнатата стойност). Дали да се изпълни методът на производния клас или този на базовия клас зависи от типа на обекта или от типа на указателя (ако се използува указател към обект), чрез който се извиква методът, и се  определя по време на компилация. Този начин на определяне е известен като "ранно свързване".

Езикът C++ поддържа още един механизъм за определяне на метода, който трябва да бъде изпълнен, наречен "късно свързване" (late binding). Kой от методите да бъде изпълнен (методът на базовия или на производния клас) в този случай се определя по време на изпълнение на програмите, а не по време на компилацията. Късното свързване се прилага само върху специален вид методи на класовете, наречени виртуални методи (използува се и терминът виртуални функции). Виртуалните методи имат следните особености:

- Виртуален метод се декларира чрез ключовата дума virtual, поставена пред неговото име.

- Статичните методи не могат да бъдат виртуални.

- Виртуални методи не могат да се декларират като приятели (friend) на други класове.

- Ако в даден клас бъде дефиниран виртуален метод и след това в производен клас на дадения клас бъде дефиниран метод със същия прототип (име, параметри и тип на върнатата стойност), то методът в производния клас ще бъде също виртуален, дори ако спецификаторът virtual бъде пропуснат.

Чрез използуване на виртуални методи се постигат два много полезни ефекта:

Първо: Ако даден виртуален метод не е дефиниран в един производен клас, но бъде извикан, то автоматично се извиква виртуалният метод от неговия базов клас. Ако и в базовия клас също не е дефиниран такъв метод търсенето продължава в следващото ниво на йерархията на класовете, докато бъде намерен извиканият метод или докато се изчерпят всички класове в йерархията.

Второ: На указател към даден клас могат да се присвояват адресите на обекти, чиито класове са производни на дадения. Извикването на виртуален метод чрез указателя води до изпълнение на този метод, който е дефиниран за обекта (по-точно за класа на обекта), към който сочи указателят в момента на извикването, а не до изпълнение на метода, дефиниран за класа, който е тип на указателя. По този начин чрез една и съща синтактична конструкция (извикване на виртуален метод чрез указател) могат да се извикват различни методи в зависимост от обекта, към който е насочен указателят.

Действието на виртуалните методи е илюстрирано в програма 7.8.

Програма 7.8. Виртуални методи

#include

#include

//Дефиниция на клас Base

class Base {

public:

virtual void message1(){cout<<"Base - message1\n";}

virtual void message2(){cout<<"Base - message2\n";}

void message3(){cout<<"Base - message3\n";}

};

//Декларация на клас Derived

class Derived:public Base{

public:

virtual void message1(){cout<<"Derived-meesage1\n"; }

void message2(int);                        //He е виртуален; Различен параметър

void message3(){cout<<"Derived-meesage3\n";}

};

//Дефиниция на метод message2 на класа Derived

void Derived::message2(int x)

{

int y;

y=x;

cout<<"у= "<

}

//Дефиниция на функция main

main()

{

int a;

Derived x;

Base y;

Base *bp = &x;    //bp е от тип Base*, а сочи към обект от клас Derived

bp->message1();   // ! ! Извикване на Derived: :message1()

bp->message2();   //Извиква Base:: message2 (). Derived::message2(int)

//Има един параметър int

bp->message3();   //Извиква Base: :message3(). He е виртуален.

bp=&y;            //bp сочи към обект от клас Base

bp->message1();   // ! ! Извикване на Base: :message1()

getch();

}

В двата класа Base и Derived е дефиниран виртуален метод   message1(). Конструкцията  bp->message1() е използувана два пъти във функцията main, т.е. има две еднакви обръщения към метод message1(). Но при първото обръщение ще бъде извикан методът Derived::message1(), а при второто - Base::message1(). Този ефект се получава в следствие на това, че методът message1() е виртуален. Това означава, че чрез конструкцията bр-> messagel() ще бъде извикан методът message1() на обекта, към който сочи указателят bр в момента на извикването. При първото извикване на метода message1() указателят bp сочи към обекта х, който е от клас Derived. Следователно ще бъде извикан методът Derived::message1(). При второто извикване на метода message1() указателят bр сочи към обекта у, който е от клас Base. Следователно второто извикване ще се отнася за метода Base::message1(). Чрез указателя bр се извиква и метода message3(), който също е дефиниран в двата класа Base и Derived, но не e виртуален. Поради тока извикването bр-> message3() се отнася за метода Base::message3(), независимо, че в момента на извикването указателят bр сочи към обект на класа Derived.

Гъвкавостта на виртуалните методи е за сметка на тяхното бързодействие. Те са по-бавни от обикновените методи, тъй като извикването им e свързано с извличане на адресите им от специални таблици (таблица на виртуалните методи, които се асоциира към всеки обект на даден клас, в който са дефинирани виртуални методи) по време на изпълнение на програмите, а не по време на компилация.

7.9. АБСТРАКТНИ КЛАСОВЕ

В класовете могат да се декларират неопределени виртуални методи. Неопределен виртуален метод е този, който има само декларация, но няма дефиниция. За да се укаже, че един виртуален метод е неопределен, се използува присвояване на стойност нула, както е показано в следния пример:

class AbstrCl {

int х,у;

public:

AbstrCl();

. . . . . . . . .

virtual void fl() = 0;                //Неопределен виртуален метод

virtual void f2 ();

. . . . . . . . .

};

Клас, в който е деклариран поне един неопределен виртуален метод, се нарича абстрактен клас. Следователно показаният по-горе клас AbstrCl е абстрактен. Обикновените класове (които нямат неопределени виртуални методи) се наричат реални.

Абстрактните класове имат някои особености, които имат характер на ограничения в сравнение с реалните класове:

- Обекти на абстрактни класове не могат да бъдат създавани.

- Абстрактните класове могат да се използуват само като базови за други класове.

- Абстрактните класове не могат да се използуват като тип на параметри и тип на върната стойност на функциите.

- Методите на абстрактните класове могат да се извикват от техните конструктори, но извикване на празен виртуален метод не е възможно и води до грешка по време на изпълнение на програмите.

- Допустимо e дефиниране и използуване на указатели към абстрактни класове и псевдоними на абстрактни класове. Изброените особености се илюстрират от следните примери:

AbstrCl х;  //ГРЕШКА ! Не могат да се създават обекти на абстрактните класове.

AbetrCl *ptr;     //Допустимо. Указатели моаат.

AbstrCl func();  //грешка ! Абстрактен клас не може да бъде тип на върната стойност

// на функция.

Float f(AbstrCl);  //ГРЕШКА! Абстрактен клас не може да бъде тип на параметър на

// функция.

AbstrCl & f(AbstrCl  &);   //Допустимо.

AbstrCl * f(AbstrCl *);      //Допустимо.

Едно характерно приложение на абстрактните класове и виртуалните методи e свързано със създаване на хетерогенни структури от данни. Примери за такива структури са списъци, графи, дървета и др., чиито елементи са обекти от различни класове.

7.10. ПОЛИМОРФИЗЪМ

Полиморфизмът е една от отличителните характеристики на обeктно-ориeнтиранитe езици. Той се изразява в това, че едни и същи действия (в общ смисъл) се реализират по различен начин в зависимост от обектите, върху които се прилагат, т.е. действията са полиморфични. В езика C++ полиморфизмът се реализира чрез виртуални методи.

За да се реализира едно полиморфично действие, класовете на обектите, върху които то ще се прилага, трябва да имат общ корен (родител или прародител), т.е. да бъдат производни на един и същи клас. В този клас трябва да бъде дефиниран виртуален метод, съответствуващ на полиморфичното действие. Във всеки от производните класове този метод може да бъде предефиниран съобразно особеностите на конкретния клас. Активирането на полиморфичното действие трябва да става чрез указател към базовия клас, на който могат да се присвояват адресите на обекти на който и да е клас в иерархията. Съгласно механизма на извикване на виртуалните методи, ще бъде изпълняван методът на съответния обект, т.е. в зависимост от обекта към който сочи указателят, ще бъде извикван един или друг метод.

Ако класовете, в които трябва да се дефинират виртуалните методи нямат общ родител, то такъв може да бъде създаден изкуствено чрез използване на един абстрактен клас. Този клас ще съдържа само неопределени виртуални методи, които трябва да се дефинират в производните класове.

еM,�O� ���кт. Дадено ястие има (съдържа) хранителни продукти. Даден хранителен продукт е част от едно или повече ястия.

Този тип връзки имат и характеристика кардиналност (cardinality). Връзката се надписва в двата края с числа (стойности или диапазон от стойности), които показват броя обекти от съответния клас, които участват в асоциацията. Възможни стойности за означаване са:

1                                    - един обект;

*                                    - неопределен брой обекти;

0..1                                - нула или един обект;

0..*                                - нула или повече обекти;

1 ..*                               - един или повече обекти.

Обогатената връзка от тип агрегация с въведена кардиналност от двете страни е показана на фиг. 4.6. Тя съответства на следната схема. Едно или повече ястия съдържат три или повече хранителни продукта. Три или повече хранителни продукта са част от едно или повече ястия.

Следващата връзка се нарича композиция и е разновидност на агрегацията. При композицията цялото напълно притежава (strongly owns) своите компоненти [53]. Всички действия с обектите от целия клас (копиране, преместване, разрушаване) влекат същите действия, които се извършват над обектите, които го съставят. Обектите компоненти са притежание на точно един определен обект от класа цяло. Затова стойността, с която се означава връзката от страната на цялото, е 1. Примерът, който е показан на фиг. 4.7, описва връзка композиция между клас Жилищен блок и клас Апартамент.

Жилищен блок има (съдържа) апартаменти. Апартаментът е част от жилищен блок.

Не е възможно един конкретен апартамент да е част от няколко жилищни блока. Той принадлежи на точно един жилищен блок. По тази причина с отчитане на кардиналността описанта връзка композиция означава: Един жилищен блок (конкретен обект) има 17 апартамента.

4.4. ЕТАП ПРОГРАМИРАНЕ

Идеята на възложителя, трансформирана в проект, намира своята практическа реализация във вид на програмна система на етап програмиране. Погрешна е представата сред част от професионалната колегия, че програмирането е най-значимият етап в жизнения цикъл на програмния продукт. Напротив, статистиката [17] сочи следното примерно разпределение на разходите за отделните етапи: проектиране - 20%, програмиране - 5%, тестуване и настройка - 25%, съпровождане - 50%. Усилията на програмистите не бива да се омаловажават, но данните показват реалния дял на етапа програмиране. Ето защо не е правилно да се обръща внимание единствено на програмистката квалификация за сметка на другите етапи в контекста на цялостния жизнен цикъл на програмките системи. Независимо от това, свидетели сме на еволюция и усъвършенстване на стиловете, методите, техниките, концепциите в програмирането. Всички те имат за цел повишаване ефективността на този труд. Тези въпроси са разгледани подробно в гл. 5. Стилове в програмирането. Друга насока за повишаване ефективността на програмисткия труд е автоматизираното генериране на програмни текстове, сведения за което се дават в т. 1.2. Автоматизирано генериране на Windows-приложения и т. 4.6. Средства за автоматизирана разработка на програмни системи.

4.5. ЕТАП ТЕСТУВАНЕ И НАСТРОЙКА

Дейностите тестуване и настройка са свързани с откриването, локализирането и отстраняването на грешки при реализацията и изпълнението на програмните системи. В началото на тази глава бяха дадени определния за грешка в програмното осигуряване и надеждност на програмното осигуряване.

Когато се говори за грешки, надеждност, тестуване и настройка, тематиката не се възприема еднозначно. Програмните системи силно се различават от гледна точка на сложността - обем първични оператори, модулна структура, период за разработка, брой програмисти, участващи в проекта. Естествено е при наличие на голямо разнообразие от програмни системи, представите за надеждност, отсъствие или наличие на грешки да се различават. Тук ще изложим общи принципи, валидни изобщо и независимо от разнородността на програмните системи, които са обект на разглеждане. На първо място какво означава тестуване?

Ще приведем и анализираме две определения за тестуване:

Определение 1: Тестуването е процес на изпълнение на програма, потвърждаващ правилността на изпълнение на програмата и потвърждаващ, че в програмата няма грешки.

Определение 2: Тестуването е процес на изпълнение на програма, с който се цели откриване на грешки в програмата.

Първото цитирано определение се оценява като напълно неправилно, тъй като то по същество е антоним на думата тестуване. Всеки програмист с опит знае, че не е възможно да се демонстрира отсъствие на грешки при изпълнение на програма. Коректно следва да се счита второто определение.

В потвърждение ще приведем и известната мисъл на Дийкстра: Тестуването на програми може да служи като доказателство за наличие на грешки в програмите, но никога не може да докаже тяхното отсъствие.

Следва класификация на видовете тестуване в зависимост от средата, в която се провежда Тестуването на програмната система [17]:

Контролно тестуване (verification testing). Опит да се открие грешка или грешки, като програмата се изпълнява в симулирана нереална среда в лабораторни условия с фиктивни недействителни данни. Този вид тестуване е известен и като алфа (а - alpha) тестуване.

Изпитателно тестуване (validation testing). Опит да се открие грешка или грешки, като програмата се изпълнява в реална среда с реални данни.Този вид тестуване е известен и като бета (6 - beta) тестуване. Навярно читателят се е сблъсквал с бета версии на операционната система MS-DOS, например версия 6.22 или у читателя са попадали компактдискове с бетаверсии на операционната среда Windows, които фирма Microsoft официално разпространява през три месеца преди официалното представяне на своите версии като Windows 95 или Windows 98. По този начин фирмата цели обратна връзка от потребителите за поведението на програмния продукт, който е в етап на тестуване и настройка.

Атестационно тестуване (certification testing). Това е авторитетна проверка за правилността на работа на една програмна система, като резултатите от изпълнението се сравняват с предварително известно множество от очаквани стойности.

Ще приведем и класификация на видовете тестове в зависимост от структурните елементи на програмната система, които се подлагат на тестуване:

Автономно тестуване (module testing, unit testing). Провежда се тестуване на отделен програмен модул в изолирана среда и независимо от всички останали модули. Автономните тестове имат отношение към вътрешната логика на модула и външната му спецификация за връзка с други модули.

Интегрално тестуване (integration testing). Провежда се с цел контролиране на връзките между компонентите на програмната система. Този вид тестуване има отношение към структурата на отделната програма и архитектурата на цялата програмна система.

Комплексно тестуване (system testing). Провежда се с цел контрол на програмната система по отношение на окончателните резултати, които се получават при изпълнение на програмната система.

По време на етапа тестуване се провежда още една дейност - настройка (debugging) на програмните продукти. Погрешна е представата, че настройката е разновидност на тестуването. Двете дейности не бива да се смесват. Между тях има връзка и разлики, които ще поясним съгласно [25].

Ако една програма очевидно не работи правилно, тя се настройва.

Ако една програма видимо работи правилно, тя се тестува.

Тестуването е дейност, с която се открива наличие на грешки.

Настройката е дейност, с която се установява точната природа на грешката. Следва локализиране на грешката и нейното отстраняване.

Казаното no-горе потвърждава, че става дума за два взаимосвързани и припокриващи се процеса. Обикновено резултатите от тестуването служат като начални данни, с които се старира настройката.

В практиката са се наложили различни подходи, методи и средства за настройка (локализация на грешки) при изпълнение на една програмна система. При [32] се прави следната класификация:

1.   Метод на грубата сила;

2.   Интелигентни методи.

Ще изброим някои типични дейности в областта на грубата сила: а/ Добавяне (вмъкване) в програмата на контролен печат. Тази идея за вмъкване на оператори за печат на междинни резултати не е лишена от смисъл, но е пример за безсистемен подход на програмиста, който просто е изчерпал всички други възможности. Ако този принцип се възприеме, ето някои препоръки: 1. Извеждане на ехопечат за всички входни данни; 2. Извеждане на информация за развитието на числовите пресмятания; 3. Извеждане на информация за логическата последователност при изпълнението на програмата; 4. Установяване на програмни флагове за включване на контролния печат само в случай, че се работи с тестовите версии на програмните системи.

б/ Извеждане съдържанието на определени области от паметта.

в/ Стъпково проследяване изпълнението оператор по оператор.

г/ Установяване на условни и/или безусловни точки на прекъсване (break points) и автоматично изпълнение с прекъсване при достигане на съответна точка на прекъсване.

Изброените дейности могат да се извършват ръчно и автоматизирано. В първия случай се изискват умствени усилия от програмиста или тестуващия инженер. Във втория случай се налага познаване на средствата за настройка, които предлагат програмните среди и продукти. Известни са програмите Debug.exe (DEBUGger), Symdeb.exe (SYMbolic DEBugger), Afd.exe (Advanced Full screen Debugger). Те се използват за тестуване и настройка на асемблерни първични текстове в среда MS-DOS. На разположение са средства за тестуване и настройка на C/C++ първични текстове - вграден дебъгер в интегрираните програмни среди и автономна програма Td.exe (Turbo Debugger) и др.

Интелигентните методи се основават на систематично формулиране на ситуациите с изграждане на множество възможни хипотези и тяхната последваща проверка.

Практическата работа по тестуване и настройка на една програмна система най-често включва елементи и подходи както от грубата сила, така и от интелигентните методи.

Подобно на работата при етапи проектиране/програмиране, и тук при тестуването и настройката са възможни различни техники (стратегии) [17]:

Тотално тестуване. Програмната система се компилира и тестува, като се проверява изпълнението поведнъж за всяка от възможните комбинации входни данни. Този подход е възможен практически само в случаи, когато комбинациите входни данни са ограничен краен брой и времето за изпълнение е приемливо кратко.

Възходящо (bottom-up) тестуване. Програмната система се компилира и тестува отдолу нагоре. Единствено модулите на най-ниско ниво се тестуват автономно и изолирано. След като тяхното тестуване завърши, извикването им трябва да е толкова надеждно, колкото е извикването на функциите от системните библиотеки или методите от библиотеките с класове на комерсиалните програмни продукти. Следва тестуване на модули, които непосредствено викат вече проверените модули. Този път тестуването не е изолирано, а съвместно с модулите от по-ниските нива. Процесът продължава, докато се достигне до върха в йерархията на програмната система.

Низходящо (top-down) тестуване. Програмната система се компилира и тестува отгоре надолу. Изолирано и автономно се тестува главният модул. След това към него се съединяват един след друг съставните модули, които главният непосредствено извиква. Получената комбинация се тестува. И тук процесът се повтаря, докато се комплектоват и проверят всички съставни модули. При низходящото тестуване е важно да се изяснят два въпроса:

а/ Как се процедира в случай, че тестуваният в момента модул извиква модул, който още не е тестуван? Липсващият модул се замества с фиктивен, който има само входна точка, евентуално контролен печат за индикация и изход в извикващата среда. Това са празни програмни единици (stubs), които в най-простия случай имат следната структура:

заглавие                                                                     - входна точка

return                                                                          - логически край

end                                                                              - физически край

б/ В каква форма се подготвят тестовите данни и как се подават на програмата? Тестовите данни се подготвят така, както се готвят входни данни за всяко друго изпълнение. При добре проектираните програми входните операции са извън главната програма и са концентрирани в самостоятелни модули. Това води донякъде до отклонение от строгия низходящ принцип.

Тестуването и настройката на програмните системи са свързани с подбирането на подходящи комбинации входни данни, с които да се тестува работоспособността на разработвания програмен продукт. Тази дейност е известна като проектиране на тестове и не е редно да се извършва от програмиста на програмата. В софтуерните фирми се формират специални групи за тестуване. В техния състав влизат високо квалифицирани специалисти (quality assurance engineers). В предговора към книгата на Стив Магуайър [16] „Как да пишем надеждни програми" Дейвид Мур дава данни за Microsoft, от които става ясно значението, което тази фирма отдава на дейността по тестуване на програмни продукти. Тестващата група през 1984 е наброявала 5 човека, а през 1993 е нараствала на повече от 500. Изобщо висококачественото проектиране на тестове е сложен и отговорен процес. При него се изисква както творчество, така и известна доза разрушителна сила на духа [17].

Основен принцип при проектирането на тестове е да се подготвят тестови данни, с които се проверява всеки клон на алгоритъма, всяка ситуация, всяка възможност, всяка комбинация допустими стойности от входни данни. Тестовете следва да обхващат всички възможности при изпълнение на програмата. Например при участък с разклонен алгоритъм е необходим тест, който ще гарантира изпълнение на всички условни преходи във всички разклонения. При участък с цикъл е необходим тест, който ще изпълни цикъла О пъти, тест, който ще изпълни цикъла 1 път, тест, който ще изпълни цикъла максимален брой пъти. Въвежда се правилото на минимален критерий - най-малко по едно изпълнение за всички клонове на алгоритъма.

Подборът на данните за тестовите поредици трябва да обхваща следните различни случаи:

1.   Работа с тестови данни - нормални случаи;

2.   Работа с тестови данни - гранични случаи;

3.   Работа с тестови данни - изключения;

4. Работа с тестови данни, известни като нулеви случаи. За аритметичните данни това е стойност нула. За символните данни това е низ от празни позиции - интервали, или низ с нулева дължина. За указателите това е специалната стойност nil в езика Pascal и NULL в езиците C/C++. Набор от данни, при които програмата не изпълнява никакви действия, се наречени нулев вариант.

Тестуването и настройката имат своя алтернатива и тя се нарича анализ
и верификация на програмни продукти.
Невъзможността да се провежда
тотално тестуване на реални програмни системи и трудностите с подбирането
на подходящи тестови поредици за всички възможни случаи и режими на
изпълнение са довели изследователите до следната алтернативна идея. Вместо
тестуване на програмните системи провежда се доказателство, че
програмите правилно изпълняват своите функции и своето действие.
Процесът на доказателство на правилността на програмното изпълнение се
нарича още аналитична верификация. Основната идея на този подход е
следната. На всеки оператор от програмата се съпоставят два булеви израза -
предикати. Единият изразява знанието за състоянието на програмата преди
изпълнението на оператора. Другият изразява знанието за състоянието на
програмата след изпълнението на оператора. Формално тези предикати се
записват като коментари в програмните текстове:
Pascal                                                                                    C/C++
{ предусловие }                                     /* предусловие */
                  
' { постусловие }                                    /* постусловие */

Поставя се задачата да се докаже истинността на постусловието при положение, че предусловието е вярно за всеки отделен оператор. Този процес се развива върху всички оператори на цялата програма. Разглеждат се оператор по оператор. Започва се от първия и се търси доказателство, че от предусловието на първия оператор следва постусловието на последния оператор на програмата Отношението между първо предусловие и последно постусловие характеризира спецификацията на програмата (какво тя прави) Тогава доказателството, че постусловието следва от предусловието, е равностойно на твърдението, че програмата ще се изпълнява в съответствие със своята спецификация. Изложената идея за аналитична верификация на програмните системи през годините не доби популярност и следва да се отнесе към теоретичните възможности и области на изследователски интерес. Подробности за този подход могат да се прочетат в [10].

4.6. СРЕДСТВА ЗА АВТОМАТИЗИРАНА РАЗРАБОТКА НА ПРОГРАМНИ СИСТЕМИ

Класическият път за реализация на една програмна система изисква след проектиране да се премине към етап програмиране. В миналото процесът програмиране се извършваше изцяло ръчно. Това е бавна, трудоемка и отнемаща много време дейност. За улеснение на проектантите и програмистите в практиката съществуват специализирани програмни средства (CASE tools - computer aided software engineering tools), с които процесът на генериране на първичния текст, представящ скелета на една програмна система, се провежда автоматизирано по данни, които са резултат от етапа проектиране. В миналото това бяха програмни средства за структурен анализ, проектиране и реализация на програмни системи (structured analysis and design methods). Съвременните CASE tools са ориентирани към обектен анализ, проектиране и реализация на програмни системи (object oriented analysis and design methods). Основни изисквания към съвремнните средства за автоматизирана разработка на програмни системи са:

а/ Наличие на графичен редактор (diagram editor) на диаграми, с който на етап проектиране се задават класовете/обектите и връзките между тях, описващи конкретния проблем, например с диаграми на езика UML;

б/ Средства за проверка на синтактична и ограничена семантична коректност на зададените диаграми;

в/ Средства за генериране на първичен текст (code generator) от описаните диаграми с възможност за избор на инструментален програмен език, например C/C++, Java, Visual Basic;

г/ Средства за генериране на съпътстваща документация (document generator), например файл с разширение .rtf;

д/ Средства за обратно преобразуване (reverse engineering facilities), които позволяват промени, направени ръчно в автоматично генерирания първичен текст, да бъдат отразени в графичните диаграми, с които започва генерацията на първичнен текст;

е/ Запомняща среда (repository), в която диаграмите и техните атрибути/ свойства се съхраняват за по-нататъшна употреба.

Популярни софтуерни продукти (CASE tools), които служат за автоматизарана разработка на програмни системи, са:

Rational Rose (http://www.rational.com);

Visual Case (http://www.stingray.com);

Class Designer (http://www.cayennesoft.com)

Pragmatica (http://www.pragsoft.com).

 

 

WWW.POCHIVKA.ORG