void
fun2()
{
unsigned long x=RandomGenerator::GetNumber();
. . .
}
Такое построение программы и удобно, и надежно. В отличие от глобальной переменной, второй раз определить генератор невозможно – мы и первый-то раз определили его лишь фактом включения класса RandomGenerator в программу, а два раза определить один и тот же класс компилятор нам не позволит.
Разумеется, существуют и другие способы сделать так, чтобы существовал только один объект какого-либо класса.
Кратко суммируем результаты этого параграфа:
-
Автоматические переменные заново создаются каждый раз, когда управление передается в соответствующую функцию или блок.
-
Статические и глобальные переменные создаются один раз, в самом начале выполнения программы.
-
К глобальным переменным можно обращаться из всей программы.
-
К статическим переменным, определенным вне функций, можно обращаться из всех функций данного файла.
-
Хотя использовать глобальные переменные иногда удобно, делать это следует с большой осторожностью, поскольку легко допустить ошибку (нет контроля доступа к ним, можно переопределить глобальную переменную).
-
Статические атрибуты класса существуют в единственном экземпляре и создаются в самом начале выполнения программы. Статические атрибуты применяют тогда, когда нужно иметь одну переменную, к которой могут обращаться все объекты данного класса. Доступ к статическим атрибутам контролируется теми же правилами, что и к обычным атрибутам.
-
Статические методы класса используются для функций, по сути являющихся глобальными, но логически относящихся к какому-либо классу.
15.5 Область видимости имен
Между именами переменных, функций, типов и т.п. при использовании одного и того же имени в разных частях программы могут возникать конфликты. Для того чтобы эти конфликты можно было разрешать, в языке существует такое понятие как область видимости имени.
Минимальной областью видимости имен является блок. Имена, определяемые в блоке, должны быть различны. При попытке объявить две переменные с одним и тем же именем произойдет ошибка. Имена, определенные в блоке, видимы (доступны) в этом блоке и во всех вложенных блоках. Аргументы функции, описанные в ее заголовке, рассматриваются как определенные в теле этой функции.
Имена, объявленные в классе, видимы внутри этого класса, т.е. во всех его методах. Для того чтобы обратиться к атрибуту класса, нужно использовать операции ".", "->" или "::".
Для имен, объявленных вне блоков, областью видимости является весь текст файла, следующий за объявлением.
Объявление может перекрывать такое же имя, объявленное во внешней области.
int x = 7;
class A
{
public:
void foo(int y);
int x;
};
int main()
{
A a;
a.foo(x);
// используется глобальная переменная x
// и передается значение 7
cout << x;
return 1;
}
void
A::foo(int y)
{
x = y + 1;
{
double x = 3.14;
cout << x;
}
cout << x;
} // x – атрибут объекта типа A
// новая переменная x перекрывает
// атрибут класса x
В результате выполнения приведенной программы будет напечатано 3.14, 8 и 7.
Несмотря на то, что имя во внутренней области видимости перекрывает имя, объявленное во внешней области, перекрываемая переменная продолжает существовать. В некоторых случаях к ней можно обратиться, явно указав область видимости с помощью квалификатора "::". Обозначение ::имя говорит о том, что имя относится к глобальной области видимости. (Попробуйте поставить :: перед переменной x в приведенном примере.) Два двоеточия часто употребляют перед именами стандартных функций библиотеки языка Си++, чтобы, во-первых, подчеркнуть, что это глобальные имена, и, во-вторых, избежать возможных конфликтов с именами методов класса, в котором они употребляются.
Если перед квалификатором поставить имя класса, то поиск имени будет производиться в указанном классе. Например, обозначение A::x показало бы, что речь идет об атрибуте класса A. Аналогично можно обращаться к атрибутам структур и объединений. Поскольку определения классов и структур могут быть вложенными, у имени может быть несколько квалификаторов:
class Example
{
public:
enum Color { RED, WHITE, BLUE };
struct Structure
static int Flag;
int x;
};
int y;
void Method();
};
Следующие обращения допустимы извне класса:
Example::BLUE
Example::Structure::Flag
При реализации метода Method обращения к тем же именам могут быть проще:
void
Example::Method()
{
Color x = BLUE;
y = Structure::flag;
}
При попытке обратиться извне класса к атрибуту набора BLUE компилятор выдаст ошибку, поскольку имя BLUE определено только в контексте класса.
Отметим одну особенность типа enum. Его атрибуты как бы экспортируются во внешнюю область имен. Несмотря на наличие фигурных скобок, к атрибутам перечисленного типа Color не обязательно (хотя и не воспрещается) обращаться Color::BLUE.
15.6 Оператор определения контекста namespace
Несмотря на столь развитую систему областей видимости имен, иногда и ее недостаточно. В больших программах возможность возникновения конфликтов на глобальном уровне достаточно реальна. Имена всех классов верхнего уровня должны быть различны. Хорошо, если вся программа разрабатывается одним человеком. А если группой? Особенно при использовании готовых библиотек классов. Чтобы избежать конфликтов, обычно договариваются о системе имен классов. Договариваться о стиле имен всегда полезно, однако проблема остается, особенно в случае разработки классов, которыми будут пользоваться другие.
Одно из сравнительно поздних добавлений к языку Си++ – контексты, определяемые с помощью оператора namespace. Они позволяют заключить группу объявлений классов, переменных и функций в отдельный контекст со своим именем. Предположим, мы разработали набор классов для вычисления различных математических функций. Все эти классы, константы и функции можно заключить в контекст math для того, чтобы, разрабатывая программу, использующую наши классы, другой программист не должен был бы выбирать имена, обязательно отличные от тех, что мы использовали.
namespace math
{
double const pi = 3.1415;
double sqrt(double x);
class Complex
{
public:
. . .
};
};
Теперь к константе pi следует обращаться math::pi.
Контекст может содержать как объявления, так и определения переменных, функций и классов. Если функция или метод определяется вне контекста, ее имя должно быть полностью квалифицировано
double math::sqrt(double x)
{
. . .
}
Контексты могут быть вложенными, соответственно, имя должно быть квалифицировано несколько раз:
namespace first
{
int i;
namespace second // первый контекст
// второй контекст
{
int i;
int whati() { return first::i; }
// возвращается значение первого i
int anotherwhat { return i; }
// возвращается значение второго i
}
first::second::whati(); // вызов функции
Если в каком-либо участке программы интенсивно используется определенный контекст, и все имена уникальны по отношению к нему, можно сократить полные имена, объявив контекст текущим с помощью оператора using.
double x = pi; // ошибка, надо использовать math::pi
using namespace math; // использовать контекст math
double y = pi; // теперь правильно
16 Обработка ошибок
16.1 Виды ошибок
Существенной частью любой программы является обработка ошибок. Прежде чем перейти к описанию средств языка Си++, предназначенных для обработки ошибок, остановимся немного на том,какие, собственно, ошибки мы будем рассматривать.
Ошибки компиляции пропустим:пока все они не исправлены, программа не готова, и запустить ее нельзя. Здесь мы будем рассматривать только ошибки, происходящие во время выполнения программы.
Первый вид ошибок, который всегда приходит в голову – это ошибки программирования. Сюда относятся ошибки в алгоритме, в логике программы и чисто программистские ошибки. Ряд возможных ошибок мы называли ранее (например, при работе с указателями), но гораздо больше вы узнаете на собственном горьком опыте.
Теоретически возможно написать программу без таких ошибок. Во многом язык Си++ помогает предотвратить ошибки во время выполнения программы,осуществляя строгий контроль на стадии компиляции. Вообще, чем строже контроль на стадии компиляции, тем меньше ошибок остается при выполнении программы.
Перечислим некоторые средства языка, которые помогут избежать ошибок:
-
Контроль типов. Случаи использования недопустимых операций и смешения несовместимых типов будут обнаружены компилятором.
-
Обязательное объявление имен до их использования. Невозможно вызвать функцию с неверным числом аргументов. При изменении определения переменной или функции легко обнаружить все места, где она используется.
-
Ограничение видимости имен, контексты имен. Уменьшается возможность конфликтов имен, неправильного переопределения имен.
Самым важным средством уменьшения вероятности ошибок является объектно-ориентированный подход к программированию,который поддерживает язык Си++. Наряду с преимуществами объектного программирования, о которых мы говорили ранее, построение программы из классов позволяет отлаживать классы по отдельности и строить программы из надежных составных "кирпичиков", используя одни и те же классы многократно.
Несмотря на все эти положительные качества языка, остается "простор" для написания ошибочных программ. По мере рассмотрения свойств языка, мы стараемся давать рекомендации, какие возможности использовать, чтобы уменьшить вероятность ошибки.
Лучше исходить из того, что идеальных программ не существует, это помогает разрабатывать более надежные программы. Самое главное – обеспечить контроль данных, а для этого необходимо проверять в программе все, что может содержать ошибку. Если в программе предполагается какое-то условие, желательно проверить его, хотя бы в начальной версии программы, до того, как можно будет на опыте убедиться, что это условие действительно выполняется. Важно также проверять указатели, передаваемые в качестве аргументов, на равенство нулю; проверять, не выходят ли индексы за границы массива и т.п.
Ну и решающими качествами, позволяющими уменьшить количество ошибок, являются внимательность, аккуратность и опыт.
Второй вид ошибок – "предусмотренные", запланированные ошибки. Если разрабатывается программа диалога с пользователем, такая программа обязана адекватно реагировать и обрабатывать неправильные нажатия клавиш. Программа чтения текста должна учитывать возможные синтаксические ошибки. Программа передачи данных по телефонной линии должна обрабатывать помехи и возможные сбои при передаче. Такие ошибки – это, вообще говоря, не ошибки с точки зрения программы, а плановые ситуации, которые она обрабатывает.
Третий вид ошибок тоже в какой-то мере предусмотрен. Это исключительные ситуации, которые могут иметь место, даже если в программе нет ошибок. Например, нехватка памяти для создания нового объекта. Или сбой диска при извлечении информации из базы данных.
Именно обработка двух последних видов ошибок и рассматривается в последующих разделах. Граница между ними довольно условна. Например, для большинства программ сбой диска – исключительная ситуация, но для операционной системы сбой диска должен быть предусмотрен и должен обрабатываться. Скорее два типа можно разграничить по тому, какая реакция программы должна быть предусмотрена. Если после плановых ошибок программа должна продолжать работать, то после исключительных ситуаций надо лишь сохранить уже вычисленные данные и завершить программу.
16.2 Возвращаемое значение как признак ошибки
Простейший способ сообщения об ошибках предполагает использование возвращаемого значения функции или метода. Функция сохранения объекта в базе данных может возвращать логическое значение: true в случае успешного сохранения, false – в случае ошибки.
class Database
{
public:
bool SaveObject(const Object&obj);
};
Соответственно, вызов метода должен выглядеть так:
if (database.SaveObject(my_obj) == false ){
//обработка ошибки
}
Обработка ошибки, разумеется, зависит от конкретной программы. Типична ситуация, когда при многократно вложенных вызовах функций обработка происходит на несколько уровней выше, чем уровень, где ошибка произошла. В таком случае результат, сигнализирующий об ошибке, придется передавать во всех вложенных вызовах.
int main()
{
if (fun1()==false ) //обработка ошибки
return 1;
}
bool
fun1()
{
if (fun2()==false )
return false ;
return true ;
}
bool
fun2()
{
if (database.SaveObject(obj)==false )
return false ;
return true ;
}
Если функция или метод должны возвращать какую-то величину в качестве результата, то особое, недопустимое, значение этой величины используется в качестве признака ошибки. Если метод возвращает указатель, выдача нулевого указателя применяется в качестве признака ошибки. Если функция вычисляет положительное число, возврат - 1 можно использовать в качестве признака ошибки.
Иногда невозможно вернуть признак ошибки в качестве возвращаемого значения. Примером является конструктор объекта, который не может вернуть значение. Как же сообщить о том, что во время инициализации объекта что-то было не так?
Распространенным решением является дополнительный атрибут объекта – флаг, отражающий состояние объекта. Предположим, конструктор класса Database должен соединиться с сервером базы данных.
class Database
{
public :
Database(const char *serverName);
...
bool Ok(void )const {return okFlag;};
private :
bool okFlag;
};
Database::Database(const char*serverName)
{
if (connect(serverName)==true )
okFlag =true ;
else
okFlag =false ;
}
int main()
{
Database database("db-server");
if (!database.Ok()){
cerr <<"Ошибка соединения с базой данных"<
return 0;
}
return 1;
}
Лучше вместо метода Ok, возвращающего значение флага okFlag, переопределить операцию ! (отрицание).
class Database
{
public :
bool operator !()const {return !okFlag;};
};
Тогда проверка успешности соединения с базой данных будет выглядеть так:
if (!database){
cerr <<"Ошибка соединения с базой
данных"<
}
Следует отметить, что лучше избегать такого построения классов, при котором возможны ошибки в конструкторе. Из конструктора можно выделить соединение с сервером базы данных в отдельный метод Open :
class Database
{
public :
Database();
bool Open(const char*serverName);
}
и тогда отпадает необходимость в операции ! или методе Ok().
Использование возвращаемого значения в качестве признака ошибки – метод почти универсальный. Он применяется, прежде всего, для обработки запланированных ошибочных ситуаций. Этот метод имеет ряд недостатков. Во-первых, приходится передавать признак ошибки через вложенные вызовы функций. Во-вторых, возникают неудобства, если метод или функция уже возвращают значение, и приходится либо модифицировать интерфейс, либо придумывать специальное "ошибочное" значение. В-третьих, логика программы оказывается запутанной из-за сплошных условных операторов if с проверкой на ошибочное значение.
16.3 Исключительные ситуации
В языке Си++ реализован специальный механизм для сообщения об ошибках – механизм исключительных ситуаций. Название, конечно же, наводит на мысль, что данный механизм предназначен, прежде всего, для оповещения об исключительных ситуациях, о которых мы говорили чуть ранее. Однако механизм исключительных ситуаций может применяться и для обработки плановых ошибок.
Исключительная ситуация возникает при выполнении оператора throw . В качестве аргумента throw задается любое значение. Это может быть значение одного из встроенных типов (число, строка символов и т.п.) или объект любого определенного в программе класса.
При возникновении исключительной ситуации выполнение текущей функции или метода немедленно прекращается, созданные к этому моменту автоматические переменные уничтожаются, и управление передается в точку, откуда была вызвана текущая функция или метод. В точке возврата создается та же самая исключительная ситуация, прекращается выполнение текущей функции или метода, уничтожаются автоматические переменные, и управление передается в точку, откуда была вызвана эта функция или метод. Происходит своего рода откат всех вызовов до тех пор, пока не завершится функция main и, соответственно, вся программа.
Предположим, из main была вызвана функция foo , которая вызвала метод Open , а он в свою очередь возбудил исключительную ситуацию:
class Database
{
public :
void Open(const char*serverName);
};
void
Database::Open(const char*serverName)
{
if (connect(serverName)==false )
throw 2;
}
foo()
{
Database database;
database.Open("db-server");
String y;
...
}
int main()
{
String x;
foo();
return 1;
}
В этом случае управление вернется в функцию foo , будет вызван деструктор объекта database , управление вернется в main , где будет вызван деструктор объекта x , и выполнение программы завершится. Таким образом, исключительные ситуации позволяют аварийно завершать программы с некоторыми возможностями очистки переменных.
В таком виде оператор throw используется для действительно исключительных ситуаций, которые практически никак не обрабатываются. Гораздо чаще даже исключительные ситуации требуется обрабатывать.
16.4 Обработка исключительных ситуаций
В программе можно объявить блок, в котором мы будем отслеживать исключительные ситуации с помощью операторов try и catch :
try {
...
}catch (тип_исключительной_операции){
...
}
Если внутри блока try возникла исключительная ситуация, то она первым делом передается в оператор catch . Тип исключительной ситуации – это тип аргумента throw . Если тип исключительной ситуации совместим с типом аргумента catch , выполняется блок catch . Тип аргумента catch совместим, если он либо совпадает с типом ситуации, либо является одним из ее базовых типов. Если тип несовместим, то происходит описанный выше откат вызовов, до тех пор, пока либо не завершится программа, либо не встретится блок catch с подходящим типом аргумента.
В блоке catch происходит обработка исключительной ситуации.
foo()
{
Database database;
int attempCount =0;
again:
try {
database.Open("dbserver");
} catch (int&ex){
cerr <<"Ошибка соединения номер "
<
if (++attemptCount <5)
goto again;
throw ;
}
String y;
...
}
Ссылка на аргумент throw передается в блок catch . Этот блок гасит исключительную ситуацию. Во время обработки в блоке catch можно создать либо ту же самую исключительную ситуацию с помощью оператора throw без аргументов, либо другую, или же не создавать никакой. В последнем случае исключительная ситуация считается погашенной, и выполнение программы продолжается после блока catch .
С одним блоком try может быть связано несколько блоков catch с разными аргументами. В этом случае исключительная ситуация последовательно "примеряется" к каждому catch до тех пор, пока аргумент не окажется совместимым. Этот блок и выполняется. Специальный вид catch
catch (...)
совместим с любым типом исключительной ситуации. Правда, в него нельзя передать аргумент.
16.5 Примеры обработки исключительных ситуаций
Механизм исключительных ситуаций предоставляет гибкие возможности для обработки ошибок, однако им надо уметь правильно пользоваться. В этом параграфе мы рассмотрим некоторые приемы обработки исключительных ситуаций.
Прежде всего, имеет смысл определить для них специальный класс. Простейшим вариантом является класс, который может хранить код ошибки:
class Exception
Достарыңызбен бөлісу: |