Главная страница

Язык программирования C++. Вводный курс. С для начинающих


Скачать 5.41 Mb.
НазваниеС для начинающих
Дата24.08.2022
Размер5.41 Mb.
Формат файлаpdf
Имя файлаЯзык программирования C++. Вводный курс.pdf
ТипДокументы
#652350
страница7 из 93
1   2   3   4   5   6   7   8   9   10   ...   93
58
template inline void swap( Array &array, int i, int j )
{ elemType tmp = array[ i ]; array[ i ] = array[ j ]; array[ j ] = tmp;
}
При каждом вызове swap() генерируется подходящая конкретизация, которая зависит от типа массива. Вот как выглядит программа, использующая шаблоны Array и ArrayRC:
#include "Array.h"
#include "ArrayRC.h" template inline void swap( Array &array, int i, int j )
{ elemType tmp = array[ i ]; array[ i ] = array[ j ]; array[ j ] = tmp;
} int main()
{
Array ia1;
ArrayRC ia2; cout << "swap() with Array ia1" << endl; int size = ia1.size(); swap( ia1, 1, size ); cout << "swap() with ArrayRC ia2" << endl; size = ia2.size(); swap( ia2, 1, size ); return 0;
}
Упражнение 2.13
Пусть мы имеем следующие объявления типов: enum Status { ... }; typedef string *Pstring;
Есть ли ошибки в приведенных ниже описаниях объектов?
#include "Array.h"
#include template class Array;

С++ для начинающих
59
(e) Array< Pstring > aps(1024);
Упражнение 2.14
Перепишите следующее определение, сделав из него шаблон класса:
}
Упражнение 2.15
Имеется следующий шаблон класса: public: explicit Example2 (elemType val=0) : _val(val) {}; bool min(elemType value) { return _val < value; } void value(elemType new_val) { _val = new_val; } void print (ostream &os) { os << _val; } private: elemType _val;
} template
(a) Array< int*& > pri(1024);
(b) Array< Array > aai(1024);
(c) Array< complex< double > > acd(1024);
(d) Array< Status > as(1024); class example1 { public: example1 (double min, double max); example1 (const double *array, int size); double& operator[] (int index); bool operator== (const example1&) const; bool insert (const double*, int); bool insert (double); double min (double) const { return _min; }; double max (double) const { return _max; }; void min (double); void max (double); int count (double value) const; private: int size; double *parray; double _min; double _max; template class Example2 {

С++ для начинающих
60
ostream& operator<<(ostream &os,const Example2 &ex)
{ ex.print(os); return os; }
Какие действия вызывают следующие инструкции?
(f) cout << "exs: " << exs << endl;
Упражнение 2.16
Пример из предыдущего упражнения накладывает определенные ограничения на типы данных, которые могут быть подставлены вместо elemType. Так, параметр конструктора имеет по умолчанию значение 0: explicit Example2 (elemType val=0) : _val(val) {};
Однако не все типы могут быть инициализированы нулем (например, тип string), поэтому определение объекта
Example2 exs("Walden"); является правильным, а
Example2 exs2; приведет к синтаксической ошибке
4
. Также ошибочным будет вызов функции min(), если для данного типа не определена операция меньше. С++ не позволяет задать ограничения для типов, подставляемых в шаблоны. Как вы думаете, было бы полезным иметь такую возможность? Если да, попробуйте придумать синтаксис задания ограничений и перепишите в нем определение класса Example2. Если нет, поясните почему.
Упражнение 2.17
Как было показано в предыдущем упражнении, попытка использовать шаблон Example2 с типом, для которого не определена операция меньше, приведет к синтаксической ошибке. Однако ошибка проявится только тогда, когда в тексте компилируемой программы действительно встретится вызов функции min(), в противном случае компиляция пройдет успешно. Как вы считаете, оправдано ли такое поведение? Не лучше ли предупредить об ошибке сразу, при обработке описания шаблона? Поясните свое мнение.
4 Вот как выглядит общее решение этой проблемы:
Example2( elemType nval = elemType() ) " _val( nval ) {}
(a) Example2*> ex1;
(b) ex1.min (&ex1);
(c) Example2 sa(1024),sb;
(d) sa = sb;
(e) Example2 exs("Walden");

С++ для начинающих
61
2.6.
Использование исключений
Исключениями называют аномальные ситуации, возникающие во время исполнения программы: невозможность открыть нужный файл или получить необходимое количество памяти, использование выходящего за границы индекса для какого-либо массива.
Обработка такого рода исключений, как правило, плохо интегрируется в основной алгоритм программы, и программисты вынуждены изобретать разные способы корректной обработки исключения, стараясь в то же время не слишком усложнить программу добавлением всевозможных проверок и дополнительных ветвей алгоритма.
С++ предоставляет стандартный способ реакции на исключения. Благодаря вынесению в отдельную часть программы кода, ответственного за проверку и обработку ошибок, значительно облегчается восприятие текста программы и сокращается ее размер. Единый синтаксис и стиль обработки исключений можно, тем не менее, приспособить к самым разнообразным нуждам и запросам.
Механизм исключений делится на две основные части: точка программы, в которой произошло исключение. Определение того факта, что при выполнении возникла какая-либо ошибка, влечет за собой возбуждение исключения. Для этого в С++ предусмотрен специальный оператор throw. Возбуждение исключения в случае невозможности открыть некоторый файл выглядит следующим образом:
}
Место программы, в котором исключение обрабатывается. При возбуждении исключения нормальное выполнение программы приостанавливается и управление передается обработчику исключения. Поиск нужного обработчика часто включает в себя раскрутку так называемого стека вызовов программы. После обработки исключения выполнение программы возобновляется, но не с того места, где произошло исключение, а с точки, следующей за обработчиком. Для определения обработчика исключения в С++ используется ключевое слово catch. Вот как может выглядеть обработчик для примера из предыдущего абзаца: catch (string exceptionMsg) { log_message (exceptionMsg); return false;
}
Каждый catch-обработчик ассоциирован с исключениями, возникающими в блоке операторов, который непосредственно предшествует обработчику и помечен ключевым словом try. Одному try-блоку могут соответствовать несколько catch-предложений, каждое из которых относится к определенному виду исключений. Приведем пример:
{ int *pstats = new int [4]; if ( !infile ) { string errMsg("
Невозможно открыть файл: "); errMsg += fileName; throw errMsg; int* stats (const int *ia, int size)

С++ для начинающих
62
try { pstats[0] = sum_it (ia,size); pstats[1] = min_val (ia,size); pstats[2] = max_val (ia,size);
} catch (string exceptionMsg) {
// код обработчика
} catch (const statsException &statsExcp) {
// код обработчика
} pstats [3] = pstats[0] / size; do_something (pstats); return pstats;
}
В данном примере в теле функции stats() три оператора заключены в try-блок, а четыре – нет. Из этих четырех операторов два способны возбудить исключения.
1) int *pstats = new int [4];
Выполнение оператора new может окончиться неудачей. Стандартная библиотека С++ предусматривает возбуждение исключения bad_alloc в случае невозможности выделить нужное количество памяти. Поскольку в примере не предусмотрен обработчик исключения bad_alloc, при его возбуждении выполнение программы закончится аварийно.
2) do_something (pstats);
Мы не знаем реализации функции do_something(). Любая инструкция этой функции, или функции, вызванной из этой функции, или функции, вызванной из функции, вызванной этой функцией, и так далее, потенциально может возбудить исключение. Если в реализации функции do_something и вызываемых из нее предусмотрен обработчик такого исключения, то выполнение stats() продолжится обычным образом. Если же такого обработчика нет, выполнение программы аварийно завершится.
Необходимо заметить, что, хотя оператор pstats [3] = pstats[0] / size; может привести к делению на ноль, в стандартной библиотеке не предусмотрен такой тип исключения.
Обратимся теперь к инструкциям, объединенным в try-блок. Если в одной из вызываемых в этом блоке функций – sum_it(), min_val() или max_val() –произойдет исключение, управление будет передано на обработчик, следующий за try-блоком и перехватывающий именно это исключение. Ни инструкция, возбудившая исключение, ни следующие за ней инструкции в try-блоке выполнены не будут. Представим себе, что при вызове функции sum_it() возбуждено исключение: throw string ("
Ошибка: adump27832");
Выполнение функции sum_it() прервется, операторы, следующие в try-блоке за вызовом этой функции, также не будут выполнены, и pstats[0] не будет инициализирована. Вместо этого возбуждается исключительное состояние и исследуются два catch-обработчика. В нашем случае выполняется catch с параметром типа string:

С++ для начинающих
63
}
После выполнения управление будет передано инструкции, следующей за последним catch
-обработчиком, относящимся к данному try-блоку. В нашем случае это pstats [3] = pstats[0] / size;
(Конечно, обработчик сам может возбуждать исключения, в том числе – того же типа. В такой ситуации будет продолжено выполнение catch-предложений, определенных в программе, вызвавшей функцию stats().)
Вот пример:
// код обработчика cerr << "stats(): исключение: "
<< exceptionMsg
<< endl; delete [] pstats; return 0;
}
В таком случае выполнение вернется в функцию, вызвавшую stats(). Будем считать, что разработчик программы предусмотрел проверку возвращаемого функцией stats() значения и корректную реакцию на нулевое значение.
Функция stats() умеет реагировать на два типа исключений: string и statsException
. Исключение любого другого типа игнорируется, и управление передается в вызвавшую функцию, а если и в ней не найдется обработчика, – то в функцию более высокого уровня, и так до функции main().При отсутствии обработчика и там, программа аварийно завершится.
Возможно задание специального обработчика, который реагирует на любой тип исключения. Синтаксис его таков:
}
(Детально обработка исключительных ситуаций рассматривается в главах 11 и 19.)
Упражнение 2.18
Какие ошибочные ситуации могут возникнуть во время выполнения следующей функции: int *alloc_and_init (string file_name)
{ ifstream infile (file_name) catch (string exceptionMsg) {
// код обработчика catch (string exceptionMsg) { catch (...) {
// обрабатывает любое исключение,
// однако ему недоступен объект, переданный
// в обработчик в инструкции throw

С++ для начинающих
64
int elem_cnt; infile >> elem_cnt; int *pi = allocate_array(elem_cnt); int elem; int index=0; while (cin >> elem) pi[index++] = elem; sort_array(pi,elem_cnt); register_data(pi); return pi;
}
Упражнение 2.19
В предыдущем примере вызываемые функции allocate_array(), sort_array() и register_data()
могут возбуждать исключения типов noMem, int и string соответственно. Перепишите функцию alloc_and_init(), вставив соответствующие блоки try и catch для обработки этих исключений. Пусть обработчики просто выводят в cerr сообщение об ошибке.
Упражнение 2.20
Усовершенствуйте функцию alloc_and_init() так, чтобы она сама возбуждала исключение в случае возникновения всех возможных ошибок (это могут быть исключения, относящиеся к вызываемым функциям allocate_array(), sort_array() и register_data() и какими-то еще операторами внутри функции alloc_and_init()).
Пусть это исключение имеет тип string и строка, передаваемая обработчику, содержит описание ошибки.
2.7.
Использование пространства имен
Предположим, что мы хотим предоставить в общее пользование наш класс Array, разработанный в предыдущих примерах. Однако не мы одни занимались этой проблемой; возможно, кем-то где-то, скажем, в одном из подразделений компании Intel был создан одноименный класс. Из-за того что имена этих классов совпадают, потенциальные пользователи не могут задействовать оба класса одновременно, они должны выбрать один из них. Эта проблема решается добавлением к имени класса некоторой строки, идентифицирующей его разработчиков, скажем, class Cplusplus_Primer_Third_Edition_Array { ... };
Конечно, это тоже не гарантирует уникальность имени, но с большой вероятностью избавит пользователя от данной проблемы. Как, однако, неудобно пользоваться столь длинными именами!
Стандарт С++ предлагает для решения проблемы совпадения имен механизм, называемый пространством имен. Каждый производитель программного обеспечения может заключить свои классы, функции и другие объекты в свое собственное пространство имен. Вот как выглядит, например, объявление нашего класса Array:

С++ для начинающих
65
}
Ключевое слово namespace задает пространство имен, определяющее видимость нашего класса и названное в данном случае Cplusplus_Primer_3E. Предположим, что у нас есть классы от других разработчиков, помещенные в другие пространства имен: namespace IBM_Canada_Laboratory { template class Array { ... }; class Matrix { ... };
} namespace Disney_Feature_Animation { class Point { ... }; template class Array { ... };
}
По умолчанию в программе видны объекты, объявленные без явного указания пространства имен; они относятся к глобальному пространству имен. Для того чтобы обратиться к объекту из другого пространства, нужно использовать его
квалифицированное имя, которое состоит из идентификатора пространства имен и идентификатора объекта, разделенных оператором разрешения области видимости (::).
Вот как выглядят обращения к объектам приведенных выше примеров:
IBM_Canada_Laboratory::Matrix mat;
Disney_Feature_Animation::Point origin(5000,5000);
Для удобства использования можно назначать псевдонимы пространствам имен.
Псевдоним выбирают коротким и легким для запоминания. Например:
// псевдонимы namespace LIB = IBM_Canada_Laboratory; namespace DFA = Disney_Feature_Animation; int main()
{
LIB::Array ia(1024);
}
Псевдонимы употребляются и для того, чтобы скрыть использование пространств имен.
Заменив псевдоним, мы можем сменить набор задействованных функций и классов, причем во всем остальном код программы останется таким же. Исправив только одну строчку в приведенном выше примере, мы получим определение уже совсем другого массива: namespace LIB = Cplusplus_Primer_3E; int main()
{
LIB::Array ia(1024);
} namespace Cplusplus_Primer_3E { template class Array { ... };
Cplusplus_Primer_3E::Array text;

С++ для начинающих
66
Конечно, чтобы это стало возможным, необходимо точное совпадение интерфейсов классов и функций, объявленных в этих пространствах имен. Представим, что класс
Array из Disney_Feature_Animation не имеет конструктора с одним параметром – размером. Тогда следующий код вызовет ошибку: namespace LIB = Disney_Feature_Animation; int main()
{
LIB::Array ia(1024);
}
Еще более удобным является способ использования простого, неквалифицированного имени для обращения к объектам, определенным в некотором пространстве имен. Для этого существует директива using:
#include "IBM_Canada_Laboratory.h" using namespace IBM_Canada_Laboratory; int main()
{
// IBM_Canada_Laboratory::Matrix
Matrix mat(4,4);
// IBM_Canada_Laboratory::Array
Array ia(1024);
// ...
}
Пространство имен IBM_Canada_Laboratory становится видимым в программе. Можно сделать видимым не все пространство, а отдельные имена внутри него (селективная директива using):
#include "IBM_Canada_Laboratory.h" using namespace IBM_Canada_Laboratory::Matrix;
// видимым становится только Matrix int main()
{
// IBM_Canada_Laboratory::Matrix
Matrix mat(4,4);
//
Ошибка: IBM_Canada_Laboratory::Array невидим
Array ia(1024);
// ...
}
Как мы уже упоминали, все компоненты стандартной библиотеки С++ объявлены внутри пространства имен std. Поэтому простого включения заголовочного файла недостаточно, чтобы напрямую пользоваться стандартными функциями и классами:
#include
// ошибка: string невидим string current_chapter = "
Обзор С++";

С++ для начинающих
67
Необходимо использовать директиву using:
#include using namespace std;
// Ok: видим string string current_chapter = "
Обзор С++";
Заметим, однако, что таким образом мы возвращаемся к проблеме “засорения” глобального пространства имен, ради решения которой и был создан механизм именованных пространств. Поэтому лучше использовать либо квалифицированное имя:
// правильно: квалифицированное имя std::string current_chapter = "
Обзор С++"; либо селективную директиву using:
#include using namespace std::string;
// Ok: string видим string current_chapter = "
Обзор С++";
Мы рекомендуем пользоваться последним способом.
В большинстве примеров этой книги директивы пространств имен были опущены. Это сделано ради сокращения размера кода, а также потому, что большинство примеров были скомпилированы компилятором, не поддерживающим пространства имен – достаточно недавнего нововведения С++. (Детали применения using-объявлений при работе с стандартной библиотекой С++ обсуждаются в разделе 8.6.)
В нижеследующих главах мы создадим еще четыре класса: String, Stack, List и модификацию Stack. Все они будут заключены в одно пространство имен –
Cplusplus_Primer_3E
. (Более подробно работа с пространствами имен рассматривается в главе 8.)
Упражнение 2.21
Дано пространство имен template class Array { ... }; template void print (Array< EType > ); class String { ... } template class List { ... };
} и текст программы:
#include namespace Exercize {

С++ для начинающих
68
const int size = 1024;
Array as (size);
List il (size);
// ...
Array *pas = new Array(as);
List *pil = new List(il); print (*pas);
}
Программа не компилируется, поскольку объявления используемых классов заключены в пространство имен Exercise. Модифицируйте код программы, используя
(a) квалифицированные имена
(b) селективную директиву using
(c) механизм псевдонимов
(d) директиву using
2.8.
Стандартный массив – это вектор
Хотя встроенный массив формально и обеспечивает механизм контейнера, он, как мы видели выше, не поддерживает семантику абстракции контейнера. До принятия стандарта C++ для программирования на таком уровне мы должны были либо приобрести нужный класс, либо реализовать его самостоятельно. Теперь же класс массива является частью стандартной библиотеки C++. Только называется он не массив, а вектор.
Разумеется, вектор реализован в виде шаблона класса. Так, мы можем написать vector ivec(10); vector svec(10);
Есть два существенных отличия нашей реализации шаблона класса Array от реализации шаблона класса vector. Первое отличие состоит в том, что вектор поддерживает как присваивание значений существующим элементам, так и вставку дополнительных элементов, то есть динамически растет во время выполнения, если программист решил воспользоваться этой его возможностью. Второе отличие более радикально и отражает существенное изменение парадигмы проектирования. Вместо того чтобы поддержать большой набор операций-членов, применимых к вектору, таких, как sort(), min(), max()
, find()и так далее, класс vector предоставляет минимальный набор: операции сравнения на равенство и на меньше, size() и empty(). Более общие операции, перечисленные выше, определены как независимые обобщенные алгоритмы.
Для использования класса vector мы должны включить соответствующий заголовочный файл.
#include int main() {

С++ для начинающих
1   2   3   4   5   6   7   8   9   10   ...   93


написать администратору сайта