Алгоритмические языки и программирование. Учебное пособие
.pdfДалее приведен образец синтаксиса (неработающая программа!):
class AClass{ |
// a class |
|
public: |
|
|
class AnError |
// exception class |
|
{ |
|
|
}; |
|
|
void Func(){ |
// a member function |
|
if( /* error condition */ ){ |
|
|
|
throw AnError(); |
// throw exception |
} |
|
|
} |
|
|
}; |
|
|
//========================================================
====== |
|
|
|
int main(){ |
// application |
||
try{ |
|
// try block |
|
|
AClass obj1; |
|
// interact with AClass objects |
|
obj1.Func(); |
|
// may cause error |
} |
|
|
|
catch(AClass::AnError) |
// exception handler |
||
{ |
// (catch block) |
||
} |
|
|
|
return 0;
}
Рассмотрим в качестве примера класс Stack с обработкой ошибок: превышение максимального количества элементов и попытка чтения из пустого стека.
class Stack{ private:
enum {MAX = 3}; |
//stack holds 3 ints |
51
int st[MAX]; |
//stack: array of integers |
||
int top; |
//index of top of stack |
||
public: |
|
|
|
class Range |
//exception class for Stack |
||
{ |
//note: empty class body |
||
}; |
|
|
|
//-------------------------------------------------------------- |
|
|
|
Stack(){ |
|
|
|
top = -1; |
|
|
|
} |
|
|
|
//-------------------------------------------------------------- |
|
|
|
void push(int var){ |
|
|
|
if(top >= MAX-1){ |
//if stack full, |
||
|
throw Range(); |
//throw exception |
|
} |
|
|
|
st[++top] = var; |
//put number on stack |
||
} |
|
|
|
//-------------------------------------------------------------- |
|
|
|
int pop(){ |
|
|
|
if(top < 0){ |
//if stack empty, |
||
|
throw Range(); |
//throw exception |
|
} |
|
|
|
return st[top--]; |
//take number off stack |
||
} |
|
|
|
}; |
|
|
|
//========================================================
======= |
|
int main(){ |
|
Stack s1; |
|
try{ |
|
s1.push(11); |
|
s1.push(22); |
|
s1.push(33); |
|
// s1.push(44); |
//oops: stack full |
52
cout << "1: " << s1.pop() << endl; cout << "2: " << s1.pop() << endl; cout << "3: " << s1.pop() << endl;
cout << "4: " << s1.pop() << endl; //oops: stack empty
}
catch(Stack::Range){ //exception handler
cout << "Exception: Stack Full or Empty" << endl;
}
cout << "Arrive here after catch (or normal exit)" << endl; return 0;
}
Разделим обработку различных исключений:
class Stack{ |
|
|
|
private: |
|
|
|
enum {MAX = 3}; |
|
//stack holds 3 ints |
|
int st[MAX]; |
|
//stack: array of integers |
|
int top; |
//index of top of stack |
||
public: |
|
|
|
class Full { }; |
|
//exception class |
|
class Empty { }; |
|
//exception class |
|
//-------------------------------------------------------------- |
|
|
|
Stack(){ |
|
//constructor |
|
top = -1; |
|
|
|
} |
|
|
|
//-------------------------------------------------------------- |
|
|
|
void push(int var){ |
//put number on stack |
||
if(top >= MAX-1){ |
//if stack full, |
||
throw Full(); |
//throw Full exception |
||
} |
|
|
|
st[++top] = var; |
|
||
}
//--------------------------------------------------------------
53
int pop() |
//take number off stack |
{ |
|
if(top < 0){ |
//if stack empty, |
throw Empty(); //throw Empty exception
}
return st[top--];
}
};
//========================================================
=====
int main(){ Stack s1; try{
s1.push(11);
s1.push(22);
s1.push(33);
// s1.push(44); //oops: stack full cout << "1: " << s1.pop() << endl;
cout << "2: " << s1.pop() << endl; cout << "3: " << s1.pop() << endl;
cout << "4: " << s1.pop() << endl; //oops: stack empty
}
catch (Stack :: Full){
cout << "Exception: Stack Full" << endl;
}
catch (Stack :: Empty){
cout << "Exception: Stack Empty" << endl;
}
return 0;
}
Классы для ошибок не обязаны быть пустыми – рассмотрим пример, в котором класс ошибки хранит информцию об ошибке.
class Distance{ |
//English Distance class |
54
private: |
|
|
|
int feet; |
|
|
|
float inches; |
|
|
|
public: |
|
|
|
//-------------------------------------------------------------- |
|
|
|
class InchesEx{ |
//exception class |
||
public: |
|
|
|
|
string origin; |
//for name of routine |
|
|
float iValue; |
//for faulty inches value |
|
|
InchesEx(string or, float in){ |
||
|
origin = or; |
//store string |
|
|
iValue = in; |
//store inches |
|
|
} |
|
|
}; |
//end of exception class |
||
//-------------------------------------------------------------- |
|
|
|
Distance(){ |
|
|
|
|
feet = 0; |
|
|
|
inches = 0.0; |
|
|
} |
|
|
|
//-------------------------------------------------------------- |
|
|
|
Distance(int ft, float in){ |
|
||
|
if(in >= 12.0){ |
|
|
|
throw InchesEx("2-arg constructor", in); |
||
|
} |
|
|
|
feet = ft; |
|
|
|
inches = in; |
|
|
} |
|
//-------------------------------------------------------------- |
|
void getdist(){ |
//get length from user |
cout << "\nEnter feet: "; cin >> feet;
cout << "Enter inches: "; cin >> inches;
if (inches >= 12.0){
55
throw InchesEx("getdist() function", inches);
} |
|
} |
|
//-------------------------------------------------------------- |
|
void showdist() { |
//display distance |
cout << feet << "\'-" << inches << '\"';
}
};
//========================================================
=
int main(){ try{
Distance dist1(17, 3.5); //2-arg constructor
Distance dist2; |
//no-arg constructor |
dist2.getdist(); |
//get value |
//display distances |
|
cout << "\ndist1 = "; |
dist1.showdist(); |
cout << "\ndist2 = "; |
dist2.showdist(); |
}
catch(Distance::InchesEx ix){ //exception handler cout << "\nInitialization error in " << ix.origin
<<".\n Inches value of " << ix.iValue
<<" is too large.";
}
return 0;
}
Наиболее частая ситуация, когда требуется обработка ошибок, – нехватка памяти для создания динамических переменных. При попытке выделить отстутствующую память генерируется исключение bad_alloc.
int main(){
const unsigned long SIZE = 10000; //memory size
char* ptr; |
//pointer to memory |
try{ |
|
56
ptr = new char[SIZE]; |
//allocate SIZE bytes |
}
catch(bad_alloc){ //exception handler
cout << "\nbad_alloc exception: can't allocate memory.\n"; return(1);
}
delete[] ptr; //deallocate memory cout << "\nMemory use is successful.\n"; return 0;
}
Наследование
Наследование – это механизм определения новых классов на основе существующих с возможностью добавления полей и методов и переопределения существующих. При работе с классами часто возникает ситуация, когда нужно описать несколько классов, у которых есть общая часть. Плохим способом будет дублирование кода. Лучше сделать один общий базовый класс, а остальные – производными от него, переопределяя и добавляя нужные отличия.
Основные цели использования наследования: моделирование отношения «Является» между объектами предметной области (иерархия объектов), выделение общей части нескольких классов (чтобы избежать дублирования), отделение интерфейса от реализации (выделение интерфейса).
Для описания того, что класс B является базовым, а класс D – производным, используется следующий синтаксис:
class B{};
class D : public B{};
Ключевое слово public говорит о том, что имеет место открытое наследование, при котором будут унаследованы уровни доступа. Существуют также private и protected уровни наследования, но они практически не используются.
57
Внутри производного класса можно переопределить методы базового, при этом останется возможность получить к ним доступ, используя имя базового класса и оператор доступа “::”.
Конструктор и деструктор, в отличие от обычных методов, не наследуются, но до выполнения конструктора производного класса автоматически будет вызван конструктор базового. Его также можно вызвать самостоятельно, указав имя класса и параметры. При вызове деструктора после работы деструктора производного класса автоматически будет вызван деструктор базового.
Виртуальные функции
Статический и динамический тип
Предположим, у нас есть базовый и производный классы: class B{
public:
void Iam(){
cout << "I am B";
}
};
class D : public B{ public:
void Iam(){
cout << "I am D";
}
};
Определим указатель на базовый тип и присвоим адрес переменной производного типа (это допустимо в силу принципа подстановки):
int main(){
B *Ptr = new D; Ptr->Iam(); return 0;
}
58
Вопрос: что выведется на экран? Ответ: «I am B». Дело в том, что у переменной Ptr получилось два типа – статический (B*) и динамический (D*). На этапе компиляции метод выбирается по статическому типу. Говорят, что происходит раннее связывание – метод, который будет вызван, определяется на этапе компиляции. Существует также позднее связывание, когда вызываемый метод определяется на этапе выполнения программы. Для этого метод должен быть описан как виртуальная функция. Здесь имеет место терминологическая путаница – мы говорим функция, несмотря на то, что речь идет о методе. Исправленный код выглядит так:
class B{ public:
virtual void Iam(){
cout << "I am B";
}
};
class D : public B{ public:
virtual void Iam(){
cout << "I am D";
}
};
int main(){
B *Ptr = new D; Ptr->Iam(); return 0;
}
Вывод на экран будет следующим: “I am D”. Нужно отметить, что не обязательно в производном классе указывать ключевое слово virtual. Метод Iam будет виртуальным, даже если это указано только в базовом классе. Но для удобства чтения кода программы рекомендуется добавлять ключевое слово virtual и в производных классах.
59
Вернемся к примеру без виртуальной функции и добавим деструкторы и очистку памяти.
class B{ public:
void Iam(){
cout << "I am B";
}
~B(){
cout << "Goodbye, B!";
}
};
class D : public B{ public:
void Iam(){
cout << "I am D";
}
~D(){
cout << "Goodbye, D!";
}
};
int main(){
B *Ptr = new D; Ptr->Iam(); delete Ptr; return 0;
}
Аналогично обычным методам, деструкторы могут быть виртуальными и невиртуальными. В данном примере в строчке delete Ptr произойдет раннее связывание, соответственно вызовется деструктор класса B, что неверно. Вывод: при наследовании деструктор обязательно должен быть виртуальным.
60
