
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf820 Часть iV » Расширенное использование С^4^
Первая структура catch отправляет сообщение объекту, запрашивая от него вывод информации, а вторая структура catch извлекает значения элемента данных объекта, а затем выводит их на печать. Первый метод лучше. Во втором случае элементы данных класса NegativeDenom также могут быть обш,едоступными.
В листинге 18.7 показана та же программа, что и в листингах 18.1 —18.6. Функ ция inverseO генерирует объекты класса ZeroDenom и NegativeDenom. Поскольку эти функции вызывает функция f ractionO, не знаюш,ая способа обработки этих исключительных ситуаций, имейте в виду, что это та функция, которая генерирует исключительные ситуации. Функция f ractionO также обозначает данные исклю чительные ситуации. Кроме того, main() должна поместить вызов fractionO в блок try и предоставить две структуры catch, по одной для каждой исключитель ной ситуации.
Листинг 18.7. Пример генерации объектов класса вместо встроенных значений
#include |
<iostream> |
|
|
#inclucle |
<cfloat> |
|
|
using namespace std; |
|
||
class MSG { |
|
// внутренние статические данные |
|
static char* data[]; |
|||
public: |
|
|
// общедоступный статический метод |
static char* msg(int n) |
|||
{ if (n<l |
I I n > 5) |
// проверка допустимости индекса |
|
|
return data[0]; |
|
|
|
else |
|
// возвращение допустимой строки |
} ; |
return data[n]; } |
||
|
|
|
|
char* MSG::data [] = { "\nBad argument to msg()\n", |
|||
"\nZero denominator isnot allowed\n\n" |
// хранилище текста |
||
"\nNegative denominator: ", |
|
||
"Enter numerator and positive\n", |
|
||
"denominator (any letter toquit): ", |
|
||
"Value ofthe fraction: " |
|
||
} : |
|
|
|
class ZeroDenom { |
// данные, передаваемые программе обработки ошибок |
||
char *msg; |
|||
public: |
|
|
// вызывается оператором throw |
ZeroDenom () |
|||
{ msg = MSG::msg(1); } |
// вызывается в блоке catch |
||
void print 0 const |
|||
{ cout « |
msg; } |
|
|
} ; |
|
|
|
class NegativeDenom { |
// закрытые данные для исключительной ситуации |
||
long val; |
|
||
char* msg; |
|
|
|
public: |
|
|
// конструктор преобразования |
NegativeDenom(long value) |
|||
|
: val(value), msg(MSG::msg(2)) { } |
||
char* getMsgO |
const |
|
|
{ return msg; } |
// общедоступные методы доступа кданным |
||
long getValO |
const |
||
{ return val; } |
|
||
} ; |
|
|
|
822 |
Часть IV • Расширенное использование С-^-ь |
исключительной ситуации. Функция должна создавать объект исключительной ситуации, используя конструктор по умолчанию. В этом варианте имеется класс ZeroDenom, который не знает, что передают его объекты. Его клиенты должны будут определять явно, какое сообщение передавать.
Трудно сказать, какой подход лучше. Как правило, первый подход (реализован ный в листинге 18.7) передает обязанности вниз к серверному классу ZeroDenom, а второй подход — вверх клиентам класса. Однако общая схема распределения информации между классами программы может сделать более привлекательным второй подход. Хотелось бы быть уверенным, что это различие запомнилось, что оно понятно и вполне осознается в программе.
class NegativeDenom : public ZeroDenom { long val;
public:
NegativeDenom(char ^message, long value) : ZeroDenom(message), val(value) { }
void print 0 const
{ cout « msg « val « "\n\n"; }
} ;
NegativeDenom был порожден из ZeroDenom. Можно ли породить ZeroDenom из NegativeDenom? В принципе возможно. С практической точки зрения, однако, это не очень хорошая идея. Класс NegativeDenom содержит больше компонентов в наборе данных, чем класс ZeroDenom.
Данные в базовом классе ZeroDenom объявляются защищенными, а не закры тыми, так что порожденный класс NegativeDenom способен осуществить доступ
к базовым данным. Если же данные ZeroDenom были бы закрытыми, то методы
вNegativeDenom должны использовать методы ZeroDenom для доступа к данным ZeroDenom. Например, можно спроектировать класс NegativeDenom.
class NegativeDenom |
public ZeroDenom |
|
long val; |
|
|
public: |
|
|
NegativeDenom(char ^message, long value) |
} |
|
: ZeroDenom(message), val(value) { |
||
void print 0 |
const |
// вызов базового метода |
{ ZeroDenom::print(); |
||
cout « val « "\n\n"; } |
|
}
Содной стороны, предполагалось, что если два алгоритма, в базовом классе
ив производном классе, содержат общие элементы, было бы замечательно под черкнуть этот факт в программе производного класса. Для этого надо вызвать метод базового класса в соответствующем методе производного класса. С другой стороны, добавление к базовому классу методов доступа, которые используются только в производном классе, это напрасная трата времени.
Когда классы исключительных ситуаций связаны наследованием, обозначение исключительной ситуации и ее генерация объектов такие же, как и для несвязан ных классов исключительной ситуации. Тем не менее отслеживание исключитель ных ситуаций может вызывать дополнительные проблемы, если только не будет уделено достаточное внимание взаимоотношениям между классами. В листин ге 18.8 показана программа из листинга 18.7, измененная таким образом, что класс NegativeDenom является производным от класса ZeroDenom.
Функции inverseO и f raction() требуют такие же исключительные ситуации, как в листинге 18.7. Однако именно функция inverse(), а не классы исключитель ных ситуаций ZeroDenom и NegativeDenom, знает, какое сообщение генерируется для каждой исключительной ситуации.
Глава 18 ^ Програмл^ировоние с обработкой ыскйЮчттвАьиык ситуаций |
823 |
||||||||
Enter |
numerator |
and positive |
|
|
|
Пример вывода для этой программы приве |
|||
11 |
0 |
|
ден на рис. 18.5. В нем использована прибли |
||||||
denominator (any letter toquit): |
|
зительно та же последовательность входных |
|||||||
Zero |
denominator |
isnot allowed |
|
|
|
||||
|
|
|
данных, что и для предыдущего варианта про |
||||||
Enter |
numerator |
and positive |
11 |
-42 |
|
граммы. |
|
||
denominator (any letter toquit): |
|
Как можно видеть, вывод программы неве |
|||||||
Negative denominator: Enter numerator and positive |
рен. Когда знаменатель отрицательный, про |
||||||||
грамма выводит соответствуюилее |
сообщение |
||||||||
denominator (any letter toquit): -11 44 |
|
||||||||
Value ofthe fraction: |
-0.25 |
|
|
|
об ошибке, но не отображает значение отрица |
||||
Enter |
numerator and positive |
|
|
|
тельного знаменателя. Вместо этого она пере |
||||
|
|
|
ходит к запросу следующего набора входных |
||||||
denominator (any letter toquit): exit |
|
|
|||||||
|
|
|
|
|
|
|
данных. Что сделано неправильно? |
|
|
Рис. 18.5. Вывод для |
программы |
|
Вспомним, что исключительную |
ситуацию |
|||||
|
из листинга |
18.8 |
|
|
|
можно сгенерировать с двумя контекстами: |
|||
|
|
|
|
|
|
|
из блока try и вне любого блока try. Когда |
||
|
|
исключительная ситуация генерируется вне блока try, функция завершает свое |
|||||||
|
|
выполнение немедленно и проверка повторяется в вызывающей программе. Вы |
|||||||
|
|
зов функции, которая |
генерирует исключительную ситуацию, может быть либо |
в рамках блока try, либо вне блоков try.
Например, функция inver.se() генерирует свои исключительные ситуации вне какого-либо блока try. Когда генерируется любая из этих исключительных ситуа ций, выполнение inverseO немедленно прекращается и управление передается вызывающей ее программе fraction(). В программе fractionO вызов inverseO, которая генерирует исключительную ситуацию, находится вне любого блока try. Именно поэтому fractionO также немедленно завершается и управление пере дается main О.
Листинг 18.8. Пример использования классов исключительных ситуаций, связанных наследованием
#inclucle |
<iostream> |
|
|
# include |
<cfloat> |
|
|
using namespace std; |
|
|
|
class MSG { |
// внутренние статические данные |
||
static char* data []; |
|
|
|
public- |
|
// общедоступный статический метод |
|
static char* msg(int n) |
|||
{ if (n < 1 I I n > 5) |
// проверка допустимости |
индекса |
|
|
return data[0]; |
|
|
else |
// возвращение допустимой |
строки |
|
|
return data[n]; } |
||
char* MSG:: data [] = { "\nBad argument tomsg()\n", |
|
||
"\nZero denominator isnot allowed\n\n", |
// область хранения текста |
||
"\nNegative denominator: ", |
|
|
"Enter numerator and positive\n",
"denominator (any letter toquit): ", "Value ofthe fraction: "
} :
class ZeroDenom { protected:
char *msg; public:
ZeroDenom (char* message) : msg(message)
{ }
826 |
Расштрвииов использование С^^-^ |
|||
public: |
|
|
/ / конструктор преобразования |
|
|
NegativeDenom(long value) |
|||
|
|
: val(value), msg(MSG: :nisg(2)) { } |
||
|
const |
char* |
whatO const |
/ / может возвратить произвольную строку |
|
{ |
return |
msg; } |
|
|
long |
getVal |
() const |
|
|
{ |
return |
val; } |
|
} |
;- |
|
|
|
Класс bacl_alloc определяется в заголовочном файле <new> или <new.h>. Его объект генерируется, когда оператор new не может выделить требуемый объем памяти из динамически распределяемой области. Пока не все компиляторы под держивают эту исключительную ситуацию. Приведем небольшой пример, в котором строится длинный связанный список блоков памяти. Он использует исключитель ную ситуацию baci_alloc. Кроме того, он проверяет, возвраш,ает ли оператор new пустой указатель.
#inclucie |
<iostream> |
|
|
|
|
|
|
/ / |
включая файлы |
||||
#include |
<exception> |
|
|
|
|
|
|
|
|
||||
#inclucle |
<new> |
|
|
|
|
|
|
|
|
|
|||
using |
namespace std; |
|
|
|
|
|
|
|
|
||||
struct |
Block |
|
|
|
|
|
|
|
|
|
|
||
{ char |
a[1000]; |
|
|
|
|
|
|
|
/ / |
блок памяти |
|||
Block* |
next; |
|
|
|
|
|
|
|
|
/ / |
присоединить перед ptr |
||
Block |
(Block* |
ptr) |
|
|
|
|
|
|
|||||
{ |
next |
= ptr; |
|
} } |
|
|
|
|
|
|
|
|
|
int mainO |
|
О, *p; int cnt = 1; |
|
|
|
||||||||
{ Block *list |
|
|
|
|
|||||||||
while (true) |
|
|
|
|
|
|
/ / |
перейти, |
пока он не завершится аварийно |
||||
{ |
try { |
|
|
|
|
|
} |
|
|
|
|
||
|
|
p = newBlock(list) |
|
|
|
/ / |
это не выполнится |
||||||
|
catch (bad_alloc &bad) |
« |
endl; |
|
|
|
|||||||
|
{ |
cout « |
|
bad.whatO |
|
/ / |
сообщение при исправлении |
||||||
|
|
exit(O); } |
|
|
|
|
|
|
|
|
|||
|
if (p - 0) |
|
|
|
|
|
|
|
/ / |
сообщение при исправлении |
|||
|
|
{ |
cout |
« |
"Out |
of |
memory\n\n"; |
exit(0 |
|
|
|||
|
l i s t |
= p; |
|
|
|
|
|
|
|
|
/ / |
успех : верх списка |
|
|
i f |
(++cnt%100 == 0) |
|
|
|
|
|
|
|
||||
|
|
cout « |
"Block |
#" |
« |
cnt « |
endl; } |
/ / |
выполнение отслеживания |
||||
while (p != 0) |
|
|
|
|
|
|
|
|
|
||||
{ |
p = p->next; |
delete l i s t ; |
l i s t |
= p; |
} |
/ / |
освобождение памяти |
||||||
return |
0; } |
|
|
|
|
|
|
|
|
|
|
Механизм исключительных ситуаций не поддерживает асинхронные исключи тельные ситуации, например прерывания. Он обрабатывает синхронные исклю чительные ситуации, возникаюш,ие в процессе последовательного выполнения, например переполнение, ошибки выхода за пределы области, ошибки выделения ресурсов и неверные входные данные. Исключительные ситуации не должны ис пользоваться для состояний, которые являются обычными для потока выполняе мых операций, например завершение выполнения одного этапа обычной обработки (окончание списка цикла) и начало другого.
Использование исключительных ситуаций языка С + 4- имеет два главных преимуш,ества. Во-первых, они обеспечивают обмен информацией между местом об наружения ошибки и местом, где можно ее исправить. Во-вторых, возврат стека в исходное состояние в процессе завершения вызванной функции и передача
Глава 18 • Программирование с обработкой исключительных ситуаций
управления обратно вызывающей функции являются безопасными. Если любая из вызванных функций размещает объекты в стеке, то их деструкторы вызываются таким образом, как если бы возврат к каждой из этих функций выполнялся обыч ным способом. Возвращаются системные ресурсы и не появляются взаимоблоки ровки и расходы ресурсов.
Операции приведения типов
Данный материал фактически не относится к этой главе. Однако его невозмож но было обсудить ранее, поскольку он основывается на расширенных понятиях наследования, шаблонов и обработки исключительных ситуаций.
Кроме того, решался вопрос о целесообразности обсуждения операций приве дения. Они были добавлены в язык C++ относительно недавно и опыт их исполь зования в программировании ограничен. Отсутствуют серьезные доказательства, что эти операции лучше, чем стандартные простые приведения, которые применя лись раньше.
Однако операции приведения типов представляют собой набор интересных идей из области программной инженерии. Рекомендуем вам познакомиться с ними. Стоит ли их использовать на практике — решайте сами.
Операции приведения типов и конструкторы преобразования ослабляют сис тему строгого контроля за типами в языке C+ + . Они расширяют возможные преобразования типов. Программисты клиентской части и программисты, осуще ствляющие сопровождение, могут не знать, какие преобразования возможны и какие из них фактически выполняются.
Чтобы помочь программистам справиться с этой ситуацией, C++ вводит не сколько дополнительных операций приведения. Область их действия шире, чем у стандартных операций приведения типов. Фактически это является одним из их преимуществ, поскольку операции приведения легче заметить в исходной программе, чем стандартные операции приведения типов.
Операция static^cast
Операция static_cast может применяться везде, где работает стандартное
•приведение типа. Она не будет использоваться там, где стандартное приведение рассматривается как слишком опасное. Представим несколько примеров.
static_cast является унарной операцией, т. е. применяемой к операнду одного типа для получения значения другого типа. Программист должен определить опе ранд (объект или выражение преобразуемого типа) в обычных скобках. Дополни тельно программист должен определить тип назначения как параметр в угловых скобках, подобно синтаксису, используемому в шаблонах.
valueOflargetlype = static_cast<TargetType>(valueOfSourceType);
Как можно заметить, это приведение типа в действительности не является унарной операцией, поскольку для него требуется как значение исходного типа (один операнд), так и имя типа назначения (второй операнд). Однако это и не бинарная операция, потому что имя приведения типов не появляется между операндами, как происходит в бинарных операциях.
Использование такого приведения типов не ограничивается только присваива нием. Оно может применяться в любом месте, где может использоваться значение типа назначения TargetType. Вот простой пример.
double d; int i = 20; |
|
d = static_cast<double>(i) ; |
/ / ok: d равно 20.0 |
828 |
Часть IV ^ Расширенное использование С-^+ |
Лучше ли это, чем старый и надежный друг для приведения типов — double? Это совершенно одинаковые веш,и.
double d; int i = 20; |
/ / o k : d равно 20.0 |
d = double(i) ; |
Рассмотрим сложный пример. Класс Account предусматривает несколько опе раций преобразования, которые извлекают значения своих компонентов. Для простоты используется массив фиксированного размера для имени владельца.
class Account { |
// базовый класс иерархии |
|
protected: |
// защищенные данные |
|
double balance; |
||
int pin; |
// идентификационный номер |
|
char owner[40]; |
|
|
public: |
|
|
Account(const char* name, int id,double bal) |
||
{ |
strcpy(owner, name) ; |
// общий |
|
balance = bal; pin = id;} |
// инициализация полей данных |
operator double () const |
||
{ |
return balance; } |
|
operator int () const |
|
|
{ |
return pin; } |
|
operator const char* () const |
|
|
{ |
return owner; } |
|
void operator -= (double amount) |
|
|
{ balance -=amount; } |
|
|
void operator += (double amount) |
// безусловное приращение |
|
{ balance += amount; } |
||
} ; |
|
|
Как уже говорилось в главе 15, эти функции перегруженных операций могут вызываться сиспользованием того же синтаксиса,что и встандартных приведе ниях типов.
Account a1("Jones",1122.5000); |
// создать объект |
int pin = (int)al; |
// допустимые приведения |
double bal = (double) a1; |
|
const char *c = (const char*) a1; |
|
Операция static_cast также действительна в этом контексте. Она выполняет
то же самое,что и стандартные операции приведения типов.
Account alC'Jones", 1122, 5000); |
// создать объект |
int pin = static_cast<int>(a1); |
// ok |
double bal = static_cast<double> (a1);
const char *c = static_cast<const char*>(a1);
He ошибитесь:операции static_cast работают только потому,что класс Account поддерживает перегруженные операции преобразования int, double и const char*. В противном случае попытка применения операции static_cast кобъек там Account была бы так же напрасна, как и попытка стандартных приведений.
Основная разница между стандартными приведениями и static_cast состоит в том, как они осуществляют преобразование указателей. Стандартные приведе
ния основываются на здравом смысле программистов. Если требуется, чтобы указатель двойной длины ссылался на переменную int,значит, имеется уважи тельная причина поступать подобным образом.
В листинге 18.9 представлен пример использования преобразований указате ля. Результаты выполнения программы показаны нарис. 18.6.